From 5cae7f7da2a9cb14667e92404f4c8cbcc74eb204 Mon Sep 17 00:00:00 2001 From: Oghenemarho Orukele Date: Sat, 16 Sep 2023 11:28:32 +0200 Subject: [PATCH 1/9] implemented create sub account route in the API client. --- README.md | 36 ++++++++------------ src/client.rs | 27 ++++++++++++++- src/error.rs | 4 +++ src/lib.rs | 2 +- src/resources/mod.rs | 2 ++ src/resources/paystack_enums.rs | 2 +- src/resources/subaccount.rs | 38 +++++++++++++++++++++ src/resources/transaction.rs | 3 +- src/resources/transaction_split.rs | 4 +-- src/response.rs | 35 +++++++++++++++++-- tests/api/transaction.rs | 16 ++++++--- tests/api/transaction_split.rs | 54 +++++++++++++++--------------- 12 files changed, 160 insertions(+), 63 deletions(-) create mode 100644 src/resources/subaccount.rs diff --git a/README.md b/README.md index fe24d89..73a1716 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Convenient **Async** rust bindings and types for the [Paystack](https://paystack.com) HTTP API aiming to support the entire API surface. Not the case? Please open an issue. I update the definitions on a weekly basis. -The client aims to make recieving payments for African business or business with African clients building with Rust as hassle-free as possible. +The client aims to make receiving payments for African business or business with African clients building with Rust as hassle-free as possible. The client currently covers the following section of the API, and the sections to be implemented in order are left unchecked: @@ -53,33 +53,25 @@ Initializing an instance of the Paystack client and creating a transaction. ```rust use std::env; use dotenv::dotenv; - use paystack::{PaystackClient, InitializeTransactionBody, Error, Currency, Channel}; + use paystack::{PaystackClient, InitializeTransactionBodyBuilder, Error, Currency, Channel}; #[tokio::main] async fn main() -> Result<(), Error>{ dotenv().ok(); let api_key = env::var("PAYSTACK_API_KEY").unwrap(); let client = PaystackClient::new(api_key); - - let body = InitializeTransactionBody { - amount: "20000".to_string(), - email: "email@example.com".to_string(), - currency: Some(Currency::NGN), - channels: Some(vec![ - Channel::ApplePay, - Channel::BankTransfer, - Channel::Bank, - ]), - bearer: None, - callback_url: None, - invoice_limit: None, - metadata: None, - plan: None, - reference: None, - split_code: None, - subaccount: None, - transaction_charge: None, - }; + + let body = InitializeTransactionBodyBuilder::default() + .amount("10000".to_string()) + .email("email@example.com".to_string()) + .currency(Some(Currency::NGN)) + .channels(Some(vec![ + Channel::ApplePay, + Channel::Bank, + Channel::BankTransfer + ])) + .build() + .unwrap(); let transaction = client .initialize_transaction(body) diff --git a/src/client.rs b/src/client.rs index cadb3f6..1e49f53 100644 --- a/src/client.rs +++ b/src/client.rs @@ -7,7 +7,7 @@ extern crate serde_json; use reqwest::StatusCode; use std::fmt::Debug; -use crate::{ChargeBody, Currency, ExportTransactionResponse, PartialDebitTransactionBody, Error, PaystackResult, ResponseWithoutData, Status, Subaccount, InitializeTransactionBody, TransactionResponse, CreateTransactionSplitBody, TransactionSplitListResponse, TransactionSplitResponse, TransactionStatus, TransactionStatusList, TransactionTimeline, TransactionTotalsResponse, put_request, UpdateTransactionSplitBody}; +use crate::{ChargeBody, Currency, ExportTransactionResponse, PartialDebitTransactionBody, Error, PaystackResult, ResponseWithoutData, Status, Subaccount, InitializeTransactionBody, TransactionResponse, CreateTransactionSplitBody, TransactionSplitListResponse, TransactionSplitResponse, TransactionStatus, TransactionStatusList, TransactionTimeline, TransactionTotalsResponse, put_request, UpdateTransactionSplitBody, CreateSubaccountBody, CreateSubAccountResponse}; use crate::{get_request, post_request}; static BASE_URL: &str = "https://api.paystack.co"; @@ -449,4 +449,29 @@ impl PaystackClient { Err(err) => Err(Error::FailedRequest(err.to_string())), } } + + + /// Creates a new subaccount. + /// + /// Takes in the following parameters + /// - body: subaccount to create. + pub async fn create_subaccount( + &self, + body: CreateSubaccountBody + ) -> PaystackResult { + let url = format!("{}/subaccount", BASE_URL); + + match post_request(&self.api_key, &url, body).await { + Ok(response) => match response.status() { + StatusCode::OK => match response.json::().await { + Ok(content) => Ok(content), + Err(err) => Err(Error::Subaccount(err.to_string())) + }, + _ => { + Err(Error::RequestNotSuccessful(response.status().to_string(), response.text().await?)) + } + }, + Err(err) => Err(Error::FailedRequest(err.to_string())) + } + } } diff --git a/src/error.rs b/src/error.rs index 2e4e511..35eb31b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -27,6 +27,10 @@ pub enum Error { #[error("Transaction Split Error: {0}")] TransactionSplit(String), + /// Error associated with Subaccount + #[error("Subaccount Error: {0}")] + Subaccount(String), + /// Error for unsuccessful request to the Paystack API #[error("Request failed - Status Code: {0} Body: {1}")] RequestNotSuccessful(String, String), diff --git a/src/lib.rs b/src/lib.rs index 6a535bb..34c33e5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ #![deny(missing_docs)] -//! Convenient rust bindings and types for the Paystakc HTTP API aiming to support the entire API surface. +//! Convenient rust bindings and types for the Paystack HTTP API aiming to support the entire API surface. //! Not the case? Please open an issue. I update the definitions on a weekly basis. //! //! # Documentation diff --git a/src/resources/mod.rs b/src/resources/mod.rs index 67ba174..59c63d9 100644 --- a/src/resources/mod.rs +++ b/src/resources/mod.rs @@ -2,9 +2,11 @@ mod charge; mod paystack_enums; mod transaction; mod transaction_split; +mod subaccount; // public re-export pub use charge::*; pub use paystack_enums::*; pub use transaction::*; pub use transaction_split::*; +pub use subaccount::*; diff --git a/src/resources/paystack_enums.rs b/src/resources/paystack_enums.rs index 24365fc..30e661b 100644 --- a/src/resources/paystack_enums.rs +++ b/src/resources/paystack_enums.rs @@ -277,4 +277,4 @@ impl fmt::Display for BearerType { }; write!(f, "{}", lowercase_string) } -} +} \ No newline at end of file diff --git a/src/resources/subaccount.rs b/src/resources/subaccount.rs new file mode 100644 index 0000000..6abb552 --- /dev/null +++ b/src/resources/subaccount.rs @@ -0,0 +1,38 @@ +//! Subaccounts +//! =========== +//! The Subaccounts API allows you create and manage subaccounts on your integration. +//! Subaccounts can be used to split payment between two accounts (your main account and a sub account). + +use serde::Serialize; +use derive_builder::Builder; + +/// This struct is used to create the body for creating a subaccount on your integration. +#[derive(Serialize, Debug, Builder)] +pub struct CreateSubaccountBody { + /// Name of business for subaccount + business_name: String, + /// Bank Code for the bank. + /// You can get the list of Bank Codes by calling the List Banks endpoint. + settlement_bank: String, + /// Bank Account Number + account_number: String, + /// The default percentage charged when receiving on behalf of this subaccount + percentage_charge: f32, + /// A description for this subaccount + description: String, + /// A contact email for the subaccount + #[builder(default = "None")] + primary_contact_email: Option, + /// A name for the contact person for this subaccount + #[builder(default = "None")] + primary_contact_name: Option, + /// A phone number to call for this subaccount + #[builder(default = "None")] + primary_contact_phone: Option, + /// Stringified JSON object. + /// Add a custom_fields attribute which has an array of objects if you would like the fields to be + /// added to your transaction when displayed on the dashboard. + /// Sample: {"custom_fields":[{"display_name":"Cart ID","variable_name": "cart_id","value": "8393"}]} + #[builder(default = "None")] + metadata: Option +} \ No newline at end of file diff --git a/src/resources/transaction.rs b/src/resources/transaction.rs index fbebe8c..1955e48 100644 --- a/src/resources/transaction.rs +++ b/src/resources/transaction.rs @@ -1,7 +1,6 @@ //! Transactions //! ============= -//! This file contains all the structs and definitions needed to -//! create a transaction using the paystack API. +//! TThe Transactions API allows you create and manage payments on your integration. use crate::{Channel, Currency}; use serde::Serialize; diff --git a/src/resources/transaction_split.rs b/src/resources/transaction_split.rs index ddce84c..0ad36fc 100644 --- a/src/resources/transaction_split.rs +++ b/src/resources/transaction_split.rs @@ -1,7 +1,7 @@ //! Transaction Split //! ================= -//! This file contains the structs and definitions need to create -//! transaction splits for the Paystack API. +//! The Transaction Splits API enables merchants split the settlement for a transaction +//! across their payout account, and one or more subaccounts. use crate::{BearerType, Currency, SplitType}; use derive_builder::Builder; diff --git a/src/response.rs b/src/response.rs index 9b05726..a80b455 100644 --- a/src/response.rs +++ b/src/response.rs @@ -309,14 +309,16 @@ pub struct SubaccountData { /// Represents a subaccount in the percentage split data. #[derive(Debug, Deserialize, Serialize)] pub struct SubaccountResponse { - /// The ID of the subaccount. - pub id: u32, + /// Integration Id of subaccount. + pub integration: Option, + /// Subaccount domain. + pub domain: Option, /// The code of the subaccount. pub subaccount_code: String, /// The name of the business associated with the subaccount. pub business_name: String, /// The description of the business associated with the subaccount. - pub description: String, + pub description: Option, /// The name of the primary contact for the business, if available. pub primary_contact_name: Option, /// The email of the primary contact for the business, if available. @@ -327,10 +329,26 @@ pub struct SubaccountResponse { pub metadata: Option, /// The percentage charge for transactions associated with the subaccount. pub percentage_charge: u32, + /// Verification status of subaccount. + pub is_verified: Option, /// The name of the settlement bank for the subaccount. pub settlement_bank: String, /// The account number of the subaccount. pub account_number: String, + /// Settlement schedule of subaccount. + pub settlement_schedule: Option, + /// Status of subaccount. + pub active: Option, + /// Migrate subaccount or not. + pub migrate: Option, + /// The ID of the subaccount. + pub id: u32, + /// Creation time of subaccount. + #[serde(rename = "createdAt")] + pub created_at: Option, + /// Last update time of subaccount. + #[serde(rename = "updatedAt")] + pub updated_at: Option, } /// Represents the JSON response containing percentage split information. @@ -352,3 +370,14 @@ pub struct ResponseWithoutData { /// The message associated with the JSON response. pub message: String, } + +/// Represents the JSON response for subaccount creation. +#[derive(Debug, Deserialize, Serialize)] +pub struct CreateSubAccountResponse { + /// The status of the JSON response. + pub status: bool, + /// The message associated with the JSON response + pub message: String, + /// Subaccount response data + pub data: SubaccountResponse, +} \ No newline at end of file diff --git a/tests/api/transaction.rs b/tests/api/transaction.rs index 44c6ad6..fbda727 100644 --- a/tests/api/transaction.rs +++ b/tests/api/transaction.rs @@ -1,7 +1,9 @@ use fake::faker::internet::en::SafeEmail; use fake::Fake; -use paystack::{Channel, Currency, InitializeTransactionBodyBuilder, PartialDebitTransactionBodyBuilder, Status}; -use rand::{Rng}; +use paystack::{ + Channel, Currency, InitializeTransactionBodyBuilder, PartialDebitTransactionBodyBuilder, Status, +}; +use rand::Rng; use crate::helpers::get_paystack_client; @@ -25,7 +27,7 @@ async fn initialize_transaction_valid() { ])) .build() .unwrap(); - println!("{:#?}", &body); + // println!("{:#?}", &body); let res = client .initialize_transaction(body) .await @@ -280,7 +282,13 @@ async fn partial_debit_transaction_passes_or_fails_depending_on_merchant_status( let body = PartialDebitTransactionBodyBuilder::default() .email(transaction.customer.unwrap().email.unwrap()) .amount("10000".to_string()) - .authorization_code(transaction.authorization.unwrap().authorization_code.unwrap()) + .authorization_code( + transaction + .authorization + .unwrap() + .authorization_code + .unwrap(), + ) .currency(Currency::NGN) .build() .unwrap(); diff --git a/tests/api/transaction_split.rs b/tests/api/transaction_split.rs index c5989e9..62be5be 100644 --- a/tests/api/transaction_split.rs +++ b/tests/api/transaction_split.rs @@ -1,51 +1,51 @@ +use fake::Fake; +use fake::faker::name::en::Name; +use paystack::{BearerType, CreateTransactionSplitBodyBuilder, Currency, SplitType}; use crate::helpers::get_paystack_client; #[tokio::test] async fn create_transaction_split_passes_with_valid_data() { + // Arrange + let client = get_paystack_client(); + // Act + let name: String = Name().fake(); + let body = CreateTransactionSplitBodyBuilder::default() + .name(name) + .split_type(SplitType::Percentage) + .currency(Currency::NGN) + .subaccounts(vec![]) + .bearer_type(BearerType::Subaccount) + .bearer_subaccount("".to_string()) + .build() + .unwrap(); + println!("{:#?}", body); + // Assert } #[tokio::test] -async fn create_transaction_split_fails_with_invalid_data() { - -} +async fn create_transaction_split_fails_with_invalid_data() {} #[tokio::test] -async fn list_transaction_splits_in_the_integration() { - -} +async fn list_transaction_splits_in_the_integration() {} #[tokio::test] -async fn fetch_a_transaction_split_in_the_integration() { - -} +async fn fetch_a_transaction_split_in_the_integration() {} #[tokio::test] -async fn update_a_transaction_split_passes_with_valid_data() { - -} +async fn update_a_transaction_split_passes_with_valid_data() {} #[tokio::test] -async fn update_a_transaction_split_fails_with_invalid_data() { - -} +async fn update_a_transaction_split_fails_with_invalid_data() {} #[tokio::test] -async fn add_a_transaction_split_subaccount_passes_with_valid_data() { - -} +async fn add_a_transaction_split_subaccount_passes_with_valid_data() {} #[tokio::test] -async fn add_a_transaction_split_subaccount_fails_with_invalid_data() { - -} +async fn add_a_transaction_split_subaccount_fails_with_invalid_data() {} #[tokio::test] -async fn remove_a_subaccount_from_a_transaction_split_passes_with_valid_data() { - -} +async fn remove_a_subaccount_from_a_transaction_split_passes_with_valid_data() {} #[tokio::test] -async fn remove_a_subaccount_from_a_transaction_split_fails_with_invalid_data() { - -} \ No newline at end of file +async fn remove_a_subaccount_from_a_transaction_split_fails_with_invalid_data() {} From b47415de4b560b59f39d367dc55afde9be64550c Mon Sep 17 00:00:00 2001 From: Oghenemarho Orukele Date: Sat, 16 Sep 2023 12:31:23 +0200 Subject: [PATCH 2/9] Tested subaccounts API route for successful case. --- .github/workflows/rust.yml | 2 ++ src/client.rs | 2 +- src/response.rs | 2 +- src/utils.rs | 45 ++++++++++------------------------ tests/api/helpers.rs | 6 +++++ tests/api/main.rs | 1 + tests/api/subaccount.rs | 37 ++++++++++++++++++++++++++++ tests/api/transaction_split.rs | 26 ++------------------ 8 files changed, 63 insertions(+), 58 deletions(-) create mode 100644 tests/api/subaccount.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e6a937e..91a2435 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -9,6 +9,8 @@ on: env: CARGO_TERM_COLOR: always PAYSTACK_API_KEY: ${{secrets.PAYSTACK_API_KEY}} + BANK_ACCOUNT: ${{secrets.BANK_ACCOUNT}} + BANK_CODE: ${{secrets.BANK_CODE}} jobs: build: diff --git a/src/client.rs b/src/client.rs index 1e49f53..156b424 100644 --- a/src/client.rs +++ b/src/client.rs @@ -463,7 +463,7 @@ impl PaystackClient { match post_request(&self.api_key, &url, body).await { Ok(response) => match response.status() { - StatusCode::OK => match response.json::().await { + StatusCode::CREATED => match response.json::().await { Ok(content) => Ok(content), Err(err) => Err(Error::Subaccount(err.to_string())) }, diff --git a/src/response.rs b/src/response.rs index a80b455..09d1b88 100644 --- a/src/response.rs +++ b/src/response.rs @@ -328,7 +328,7 @@ pub struct SubaccountResponse { /// Additional metadata associated with the subaccount, if available. pub metadata: Option, /// The percentage charge for transactions associated with the subaccount. - pub percentage_charge: u32, + pub percentage_charge: f32, /// Verification status of subaccount. pub is_verified: Option, /// The name of the settlement bank for the subaccount. diff --git a/src/utils.rs b/src/utils.rs index 284c59b..8317368 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -3,9 +3,8 @@ //! This file contains utility sections that are used in different sections of the client use std::fmt::Debug; -use reqwest::{Client, StatusCode, Response}; +use reqwest::{Client, Response, Error}; use serde::Serialize; -use crate::{PaystackResult, Error}; /// A function for sending GET request to a specified url /// with optional query parameters using reqwest client. @@ -13,7 +12,7 @@ pub async fn get_request( api_key: &String, url: &String, query: Option>, -) -> PaystackResult { +) -> Result { let client = Client::new(); let response = client .get(url) @@ -24,17 +23,14 @@ pub async fn get_request( .await; match response { - Ok(response) => match response.status() { - StatusCode::OK => Ok(response), - _ => Err(Error::RequestNotSuccessful(response.status().to_string(), response.text().await?)) - }, - Err(err) => Err(Error::Generic(err.to_string())), + Ok(response) => Ok(response), + Err(err) => Err(err), } } /// A function for sending POST requests to a specified url /// using the reqwest client. -pub async fn post_request(api_key: &String, url: &String, body: T) -> PaystackResult +pub async fn post_request(api_key: &String, url: &String, body: T) -> Result where T: Debug + Serialize, { @@ -48,19 +44,14 @@ pub async fn post_request(api_key: &String, url: &String, body: T) -> Paystac .await; match response { - Ok(response) => match response.status() { - StatusCode::OK => Ok(response), - _ => { - Err(Error::RequestNotSuccessful(response.status().to_string(), response.text().await?)) - } - }, - Err(err) => Err(Error::Generic(err.to_string())), + Ok(response) => Ok(response), + Err(err) => Err(err), } } /// A function for sending PUT requests to a specified url /// using the reqwest client. -pub async fn put_request(api_key: &String, url: &String, body: T) -> PaystackResult +pub async fn put_request(api_key: &String, url: &String, body: T) -> Result where T: Debug + Serialize, { @@ -74,19 +65,14 @@ pub async fn put_request(api_key: &String, url: &String, body: T) -> Paystack .await; match response { - Ok(response) => match response.status() { - StatusCode::OK => Ok(response), - _ => { - Err(Error::RequestNotSuccessful(response.status().to_string(), response.text().await?)) - } - }, - Err(err) => Err(Error::Generic(err.to_string())), + Ok(response) => Ok(response), + Err(err) => Err(err), } } /// A function for sending DELETE requests to a specified url /// using the reqwest client. -pub async fn delete_request(api_key: &String, url: &String, body: T) -> PaystackResult +pub async fn delete_request(api_key: &String, url: &String, body: T) -> Result where T: Debug + Serialize, { @@ -100,12 +86,7 @@ pub async fn delete_request(api_key: &String, url: &String, body: T) -> Payst .await; match response { - Ok(response) => match response.status() { - StatusCode::OK => Ok(response), - _ => { - Err(Error::RequestNotSuccessful(response.status().to_string(), response.text().await?)) - } - }, - Err(err) => Err(Error::Generic(err.to_string())), + Ok(response) => Ok(response), + Err(err) => Err(err), } } \ No newline at end of file diff --git a/tests/api/helpers.rs b/tests/api/helpers.rs index afab743..ec07105 100644 --- a/tests/api/helpers.rs +++ b/tests/api/helpers.rs @@ -12,3 +12,9 @@ fn get_api_key() -> String { pub fn get_paystack_client() -> PaystackClient { PaystackClient::new(get_api_key()) } + +pub fn get_bank_account_number_and_code() -> (String, String) { + dotenv().ok(); + + (env::var("BANK_ACCOUNT").unwrap(), env::var("BANK_CODE").unwrap()) +} \ No newline at end of file diff --git a/tests/api/main.rs b/tests/api/main.rs index 76a7f3f..895bcf1 100644 --- a/tests/api/main.rs +++ b/tests/api/main.rs @@ -2,3 +2,4 @@ pub mod charge; pub mod helpers; pub mod transaction; pub mod transaction_split; +mod subaccount; diff --git a/tests/api/subaccount.rs b/tests/api/subaccount.rs new file mode 100644 index 0000000..a392d76 --- /dev/null +++ b/tests/api/subaccount.rs @@ -0,0 +1,37 @@ +use fake::Fake; +use fake::faker::company::en::CompanyName; +use fake::faker::lorem::en::{Sentence}; +use paystack::{CreateSubaccountBodyBuilder}; +use crate::helpers::{get_bank_account_number_and_code, get_paystack_client}; + +#[tokio::test] +async fn create_subaccount_passes_with_valid_data() { + // Arrange + let client = get_paystack_client(); + + // Act + // To test this, we need a life bank account, use the .env file for this + let business_name: String = CompanyName().fake(); + let description: String = Sentence(5..10).fake(); + let (account_number, bank_code) = get_bank_account_number_and_code(); + + let body = CreateSubaccountBodyBuilder::default() + .business_name(business_name) + .settlement_bank(bank_code.clone()) + .account_number(account_number.clone()) + .percentage_charge(18.2) + .description(description) + .build() + .unwrap(); + + // println!("{:#?}", body); + let res = client. + create_subaccount(body) + .await + .expect("Unable to Create a subaccount"); + + // Assert + assert!(res.status); + assert_eq!(res.data.settlement_bank, "Kuda Bank"); + assert_eq!(res.data.account_number, account_number) +} \ No newline at end of file diff --git a/tests/api/transaction_split.rs b/tests/api/transaction_split.rs index 62be5be..70613f3 100644 --- a/tests/api/transaction_split.rs +++ b/tests/api/transaction_split.rs @@ -1,27 +1,5 @@ -use fake::Fake; -use fake::faker::name::en::Name; -use paystack::{BearerType, CreateTransactionSplitBodyBuilder, Currency, SplitType}; -use crate::helpers::get_paystack_client; - -#[tokio::test] -async fn create_transaction_split_passes_with_valid_data() { - // Arrange - let client = get_paystack_client(); - - // Act - let name: String = Name().fake(); - let body = CreateTransactionSplitBodyBuilder::default() - .name(name) - .split_type(SplitType::Percentage) - .currency(Currency::NGN) - .subaccounts(vec![]) - .bearer_type(BearerType::Subaccount) - .bearer_subaccount("".to_string()) - .build() - .unwrap(); - println!("{:#?}", body); - // Assert -} +#[tokio::test] +async fn create_transaction_split_passes_with_valid_data() {} #[tokio::test] async fn create_transaction_split_fails_with_invalid_data() {} From 556c0842acf0fa0ddb13b8256d69afcff8e71f16 Mon Sep 17 00:00:00 2001 From: Oghenemarho Orukele Date: Sat, 16 Sep 2023 21:08:48 +0200 Subject: [PATCH 3/9] Tested subaccount creation for failure case. --- tests/api/subaccount.rs | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/tests/api/subaccount.rs b/tests/api/subaccount.rs index a392d76..abe2eba 100644 --- a/tests/api/subaccount.rs +++ b/tests/api/subaccount.rs @@ -25,8 +25,8 @@ async fn create_subaccount_passes_with_valid_data() { .unwrap(); // println!("{:#?}", body); - let res = client. - create_subaccount(body) + let res = client + .create_subaccount(body) .await .expect("Unable to Create a subaccount"); @@ -34,4 +34,35 @@ async fn create_subaccount_passes_with_valid_data() { assert!(res.status); assert_eq!(res.data.settlement_bank, "Kuda Bank"); assert_eq!(res.data.account_number, account_number) +} + +#[tokio::test] +async fn create_subaccount_fails_with_invalid_data() { + // Arrange + let client = get_paystack_client(); + + // Act + let body = CreateSubaccountBodyBuilder::default() + .business_name("".to_string()) + .settlement_bank("".to_string()) + .account_number("".to_string()) + .description("".to_string()) + .percentage_charge(0.0) + .build() + .unwrap(); + + let res = client + .create_subaccount(body) + .await; + + // Assert + match res { + Ok(_) => (), + Err(e) => { + let res = e.to_string(); + // dbg!("{:#?}", &res); + assert!(res.contains("Status Code: 400 Bad Request")); + assert!(res.contains("Account number is required")) + } + } } \ No newline at end of file From 2a8e3833af1b413d05a1d93f3d565159641560e0 Mon Sep 17 00:00:00 2001 From: Oghenemarho Orukele Date: Mon, 18 Sep 2023 08:39:38 +0200 Subject: [PATCH 4/9] fixed typos --- README.md | 3 +- examples/transaction.rs | 2 +- src/client.rs | 185 +++++++++++++++++++---------------- src/error.rs | 1 - src/resources/transaction.rs | 4 +- tests/api/subaccount.rs | 14 ++- 6 files changed, 111 insertions(+), 98 deletions(-) diff --git a/README.md b/README.md index 73a1716..9b1a7fc 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,8 @@ Initializing an instance of the Paystack client and creating a transaction. .initialize_transaction(body) .await .expect("Unable to create transaction"); - Ok(()) + + Ok(()) } ``` diff --git a/examples/transaction.rs b/examples/transaction.rs index e6db0ea..ebc6bae 100644 --- a/examples/transaction.rs +++ b/examples/transaction.rs @@ -27,7 +27,7 @@ async fn main() { .channels(Some(vec![ Channel::ApplePay, Channel::Bank, - Channel::BankTransfer + Channel::BankTransfer, ])) .build() .unwrap(); diff --git a/src/client.rs b/src/client.rs index 156b424..f3c5f95 100644 --- a/src/client.rs +++ b/src/client.rs @@ -5,10 +5,17 @@ extern crate reqwest; extern crate serde_json; +use crate::{get_request, post_request}; +use crate::{ + put_request, ChargeBody, CreateSubAccountResponse, CreateSubaccountBody, + CreateTransactionSplitBody, Currency, Error, ExportTransactionResponse, + InitializeTransactionBody, PartialDebitTransactionBody, PaystackResult, ResponseWithoutData, + Status, Subaccount, TransactionResponse, TransactionSplitListResponse, + TransactionSplitResponse, TransactionStatus, TransactionStatusList, TransactionTimeline, + TransactionTotalsResponse, UpdateTransactionSplitBody, +}; use reqwest::StatusCode; use std::fmt::Debug; -use crate::{ChargeBody, Currency, ExportTransactionResponse, PartialDebitTransactionBody, Error, PaystackResult, ResponseWithoutData, Status, Subaccount, InitializeTransactionBody, TransactionResponse, CreateTransactionSplitBody, TransactionSplitListResponse, TransactionSplitResponse, TransactionStatus, TransactionStatusList, TransactionTimeline, TransactionTotalsResponse, put_request, UpdateTransactionSplitBody, CreateSubaccountBody, CreateSubAccountResponse}; -use crate::{get_request, post_request}; static BASE_URL: &str = "https://api.paystack.co"; @@ -25,9 +32,7 @@ impl PaystackClient { /// It takes the following parameters: /// - key: Paystack API key. pub fn new(key: String) -> Self { - Self { - api_key: key, - } + Self { api_key: key } } /// This method initializes a new transaction using the Paystack API. @@ -45,9 +50,10 @@ impl PaystackClient { Ok(content) => Ok(content), Err(err) => Err(Error::Transaction(err.to_string())), }, - _ => { - Err(Error::RequestNotSuccessful(response.status().to_string(), response.text().await?)) - } + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), }, Err(err) => Err(Error::FailedRequest(err.to_string())), } @@ -69,9 +75,10 @@ impl PaystackClient { Ok(content) => Ok(content), Err(err) => Err(Error::Transaction(err.to_string())), }, - _ => { - Err(Error::RequestNotSuccessful(response.status().to_string(), response.text().await?)) - } + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), }, Err(err) => Err(Error::FailedRequest(err.to_string())), } @@ -100,9 +107,10 @@ impl PaystackClient { Ok(content) => Ok(content), Err(err) => Err(Error::Transaction(err.to_string())), }, - _ => { - Err(Error::RequestNotSuccessful(response.status().to_string(), response.text().await?)) - } + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), }, Err(err) => Err(Error::FailedRequest(err.to_string())), } @@ -123,9 +131,10 @@ impl PaystackClient { Ok(content) => Ok(content), Err(err) => Err(Error::Transaction(err.to_string())), }, - _ => { - Err(Error::RequestNotSuccessful(response.status().to_string(), response.text().await?)) - } + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), }, Err(err) => Err(Error::FailedRequest(err.to_string())), } @@ -134,18 +143,22 @@ impl PaystackClient { /// All authorizations marked as reusable can be charged with this endpoint whenever you need to receive payments /// /// This function takes a Charge Struct as parameter - pub async fn charge_authorization(&self, charge: ChargeBody) -> PaystackResult { + pub async fn charge_authorization( + &self, + charge: ChargeBody, + ) -> PaystackResult { let url = format!("{}/transaction/charge_authorization", BASE_URL); - match post_request(&self.api_key,&url, charge).await { + match post_request(&self.api_key, &url, charge).await { Ok(response) => match response.status() { StatusCode::OK => match response.json::().await { Ok(content) => Ok(content), Err(err) => Err(Error::Charge(err.to_string())), }, - _ => { - Err(Error::RequestNotSuccessful(response.status().to_string(), response.text().await?)) - } + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), }, Err(err) => Err(Error::FailedRequest(err.to_string())), } @@ -181,9 +194,10 @@ impl PaystackClient { Ok(content) => Ok(content), Err(err) => Err(Error::Transaction(err.to_string())), }, - _ => { - Err(Error::RequestNotSuccessful(response.status().to_string(), response.text().await?)) - } + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), }, Err(err) => Err(Error::FailedRequest(err.to_string())), } @@ -200,14 +214,14 @@ impl PaystackClient { match get_request(&self.api_key, &url, None).await { Ok(response) => match response.status() { - StatusCode::OK => match response.json::().await - { + StatusCode::OK => match response.json::().await { Ok(content) => Ok(content), Err(err) => Err(Error::Transaction(err.to_string())), }, - _ => { - Err(Error::RequestNotSuccessful(response.status().to_string(), response.text().await?)) - } + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), }, Err(err) => Err(Error::FailedRequest(err.to_string())), } @@ -241,14 +255,14 @@ impl PaystackClient { match get_request(&self.api_key, &url, Some(query)).await { Ok(response) => match response.status() { - StatusCode::OK => match response.json::().await - { + StatusCode::OK => match response.json::().await { Ok(content) => Ok(content), Err(err) => Err(Error::Transaction(err.to_string())), }, - _ => { - Err(Error::RequestNotSuccessful(response.status().to_string(), response.text().await?)) - } + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), }, Err(err) => Err(Error::FailedRequest(err.to_string())), } @@ -271,9 +285,10 @@ impl PaystackClient { Ok(content) => Ok(content), Err(err) => Err(Error::Transaction(err.to_string())), }, - _ => { - Err(Error::RequestNotSuccessful(response.status().to_string(), response.text().await?)) - } + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), }, Err(err) => Err(Error::FailedRequest(err.to_string())), } @@ -290,15 +305,14 @@ impl PaystackClient { match post_request(&self.api_key, &url, split_body).await { Ok(response) => match response.status() { - StatusCode::OK => { - match response.json::().await { - Ok(content) => Ok(content), - Err(err) => Err(Error::TransactionSplit(err.to_string())), - } - } - _ => { - Err(Error::RequestNotSuccessful(response.status().to_string(), response.text().await?)) - } + StatusCode::OK => match response.json::().await { + Ok(content) => Ok(content), + Err(err) => Err(Error::TransactionSplit(err.to_string())), + }, + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), }, Err(err) => Err(Error::FailedRequest(err.to_string())), } @@ -329,15 +343,14 @@ impl PaystackClient { match get_request(&self.api_key, &url, Some(query)).await { Ok(response) => match response.status() { - StatusCode::OK => { - match response.json::().await { - Ok(content) => Ok(content), - Err(err) => Err(Error::TransactionSplit(err.to_string())), - } - } - _ => { - Err(Error::RequestNotSuccessful(response.status().to_string(), response.text().await?)) - } + StatusCode::OK => match response.json::().await { + Ok(content) => Ok(content), + Err(err) => Err(Error::TransactionSplit(err.to_string())), + }, + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), }, Err(err) => Err(Error::FailedRequest(err.to_string())), } @@ -355,15 +368,14 @@ impl PaystackClient { match get_request(&self.api_key, &url, None).await { Ok(response) => match response.status() { - StatusCode::OK => { - match response.json::().await { - Ok(content) => Ok(content), - Err(err) => Err(Error::TransactionSplit(err.to_string())), - } - } - _ => { - Err(Error::RequestNotSuccessful(response.status().to_string(), response.text().await?)) - } + StatusCode::OK => match response.json::().await { + Ok(content) => Ok(content), + Err(err) => Err(Error::TransactionSplit(err.to_string())), + }, + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), }, Err(err) => Err(Error::FailedRequest(err.to_string())), } @@ -390,7 +402,10 @@ impl PaystackClient { Ok(content) => Ok(content), Err(err) => Err(Error::TransactionSplit(err.to_string())), }, - _ => Err(Error::RequestNotSuccessful(response.status().to_string(), response.text().await?)), + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), }, Err(err) => Err(Error::FailedRequest(err.to_string())), } @@ -410,15 +425,14 @@ impl PaystackClient { match post_request(&self.api_key, &url, body).await { Ok(response) => match response.status() { - StatusCode::OK => { - match response.json::().await { - Ok(content) => Ok(content), - Err(err) => Err(Error::TransactionSplit(err.to_string())), - } - } - _ => { - Err(Error::RequestNotSuccessful(response.status().to_string(), response.text().await?)) - } + StatusCode::OK => match response.json::().await { + Ok(content) => Ok(content), + Err(err) => Err(Error::TransactionSplit(err.to_string())), + }, + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), }, Err(err) => Err(Error::FailedRequest(err.to_string())), } @@ -442,22 +456,22 @@ impl PaystackClient { Ok(content) => Ok(content), Err(err) => Err(Error::TransactionSplit(err.to_string())), }, - _ => { - Err(Error::RequestNotSuccessful(response.status().to_string(), response.text().await?)) - } + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), }, Err(err) => Err(Error::FailedRequest(err.to_string())), } } - /// Creates a new subaccount. /// /// Takes in the following parameters /// - body: subaccount to create. pub async fn create_subaccount( &self, - body: CreateSubaccountBody + body: CreateSubaccountBody, ) -> PaystackResult { let url = format!("{}/subaccount", BASE_URL); @@ -465,13 +479,14 @@ impl PaystackClient { Ok(response) => match response.status() { StatusCode::CREATED => match response.json::().await { Ok(content) => Ok(content), - Err(err) => Err(Error::Subaccount(err.to_string())) + Err(err) => Err(Error::Subaccount(err.to_string())), }, - _ => { - Err(Error::RequestNotSuccessful(response.status().to_string(), response.text().await?)) - } + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), }, - Err(err) => Err(Error::FailedRequest(err.to_string())) + Err(err) => Err(Error::FailedRequest(err.to_string())), } } } diff --git a/src/error.rs b/src/error.rs index 35eb31b..5a5f05a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -2,7 +2,6 @@ //! ======== //! This file contains the structs and definitions of the errors in this crate. - /// Custom Error for the Paystack API #[derive(thiserror::Error, Debug)] #[non_exhaustive] diff --git a/src/resources/transaction.rs b/src/resources/transaction.rs index 1955e48..dbbd01a 100644 --- a/src/resources/transaction.rs +++ b/src/resources/transaction.rs @@ -3,8 +3,8 @@ //! TThe Transactions API allows you create and manage payments on your integration. use crate::{Channel, Currency}; -use serde::Serialize; use derive_builder::Builder; +use serde::Serialize; /// This struct is used to create a transaction body for creating a transaction using the Paystack API. /// This struct should be created using the `InitializeTransactionBodyBuilder` @@ -49,7 +49,7 @@ pub struct InitializeTransactionBody { transaction_charge: Option, /// Who bears Paystack charges? `account` or `subaccount` (defaults to account). #[builder(default = "None")] - bearer: Option + bearer: Option, } /// This struct is used to create a partial debit transaction body for creating a partial debit using the Paystack API. diff --git a/tests/api/subaccount.rs b/tests/api/subaccount.rs index abe2eba..6e059c5 100644 --- a/tests/api/subaccount.rs +++ b/tests/api/subaccount.rs @@ -1,8 +1,8 @@ -use fake::Fake; -use fake::faker::company::en::CompanyName; -use fake::faker::lorem::en::{Sentence}; -use paystack::{CreateSubaccountBodyBuilder}; use crate::helpers::{get_bank_account_number_and_code, get_paystack_client}; +use fake::faker::company::en::CompanyName; +use fake::faker::lorem::en::Sentence; +use fake::Fake; +use paystack::CreateSubaccountBodyBuilder; #[tokio::test] async fn create_subaccount_passes_with_valid_data() { @@ -51,9 +51,7 @@ async fn create_subaccount_fails_with_invalid_data() { .build() .unwrap(); - let res = client - .create_subaccount(body) - .await; + let res = client.create_subaccount(body).await; // Assert match res { @@ -65,4 +63,4 @@ async fn create_subaccount_fails_with_invalid_data() { assert!(res.contains("Account number is required")) } } -} \ No newline at end of file +} From 423cd3dad86e37d2fa3825f8ccfa6ae25e717eb4 Mon Sep 17 00:00:00 2001 From: Oghenemarho Orukele Date: Mon, 18 Sep 2023 09:02:28 +0200 Subject: [PATCH 5/9] moved all transaction objects to their own file --- examples/transaction.rs | 1 + src/client.rs | 275 ++--------------------------------- src/resources/transaction.rs | 275 ++++++++++++++++++++++++++++++++++- src/utils.rs | 28 ++-- tests/api/transaction.rs | 4 +- 5 files changed, 302 insertions(+), 281 deletions(-) diff --git a/examples/transaction.rs b/examples/transaction.rs index ebc6bae..b5b1b55 100644 --- a/examples/transaction.rs +++ b/examples/transaction.rs @@ -33,6 +33,7 @@ async fn main() { .unwrap(); let transaction = client + .transaction .initialize_transaction(body) .await .expect("Unable to create transaction"); diff --git a/src/client.rs b/src/client.rs index f3c5f95..f3a8047 100644 --- a/src/client.rs +++ b/src/client.rs @@ -5,14 +5,11 @@ extern crate reqwest; extern crate serde_json; -use crate::{get_request, post_request}; +use crate::{get_request, post_request, Transaction}; use crate::{ - put_request, ChargeBody, CreateSubAccountResponse, CreateSubaccountBody, - CreateTransactionSplitBody, Currency, Error, ExportTransactionResponse, - InitializeTransactionBody, PartialDebitTransactionBody, PaystackResult, ResponseWithoutData, - Status, Subaccount, TransactionResponse, TransactionSplitListResponse, - TransactionSplitResponse, TransactionStatus, TransactionStatusList, TransactionTimeline, - TransactionTotalsResponse, UpdateTransactionSplitBody, + put_request, CreateSubAccountResponse, CreateSubaccountBody, CreateTransactionSplitBody, Error, + PaystackResult, ResponseWithoutData, Subaccount, TransactionSplitListResponse, + TransactionSplitResponse, UpdateTransactionSplitBody, }; use reqwest::StatusCode; use std::fmt::Debug; @@ -24,6 +21,8 @@ static BASE_URL: &str = "https://api.paystack.co"; #[derive(Clone, Debug)] pub struct PaystackClient { api_key: String, + /// Transaction API object + pub transaction: Transaction, } impl PaystackClient { @@ -32,265 +31,11 @@ impl PaystackClient { /// It takes the following parameters: /// - key: Paystack API key. pub fn new(key: String) -> Self { - Self { api_key: key } - } - - /// This method initializes a new transaction using the Paystack API. - /// - /// It takes a Transaction type as its parameter - pub async fn initialize_transaction( - &self, - transaction_body: InitializeTransactionBody, - ) -> PaystackResult { - let url = format!("{}/transaction/initialize", BASE_URL); - - match post_request(&self.api_key, &url, transaction_body).await { - Ok(response) => match response.status() { - StatusCode::OK => match response.json::().await { - Ok(content) => Ok(content), - Err(err) => Err(Error::Transaction(err.to_string())), - }, - _ => Err(Error::RequestNotSuccessful( - response.status().to_string(), - response.text().await?, - )), - }, - Err(err) => Err(Error::FailedRequest(err.to_string())), - } - } - - /// This method confirms the status of a transaction. - /// - /// It takes the following parameters: - /// - reference: The transaction reference used to initiate the transaction - pub async fn verify_transaction( - &self, - reference: &String, - ) -> PaystackResult { - let url = format!("{}/transaction/verify/{}", BASE_URL, reference); - - match get_request(&self.api_key, &url, None).await { - Ok(response) => match response.status() { - StatusCode::OK => match response.json::().await { - Ok(content) => Ok(content), - Err(err) => Err(Error::Transaction(err.to_string())), - }, - _ => Err(Error::RequestNotSuccessful( - response.status().to_string(), - response.text().await?, - )), - }, - Err(err) => Err(Error::FailedRequest(err.to_string())), - } - } - - /// This method returns a Vec of transactions carried out on your integrations. - /// - /// The method takes the following parameters: - /// - perPage (Optional): Number of transactions to return. If None is passed as the parameter, the last 10 transactions are returned. - /// - status (Optional): Filter transactions by status, defaults to Success if no status is passed. - /// - pub async fn list_transactions( - &self, - number_of_transactions: Option, - status: Option, - ) -> PaystackResult { - let url = format!("{}/transaction", BASE_URL); - let query = vec![ - ("perPage", number_of_transactions.unwrap_or(10).to_string()), - ("status", status.unwrap_or(Status::Success).to_string()), - ]; - - match get_request(&self.api_key, &url, Some(query)).await { - Ok(response) => match response.status() { - StatusCode::OK => match response.json::().await { - Ok(content) => Ok(content), - Err(err) => Err(Error::Transaction(err.to_string())), - }, - _ => Err(Error::RequestNotSuccessful( - response.status().to_string(), - response.text().await?, - )), - }, - Err(err) => Err(Error::FailedRequest(err.to_string())), - } - } - - /// Get details of a transaction carried out on your integration - /// - /// This methods take the Id of the desired transaction as a parameter - pub async fn fetch_transactions( - &self, - transaction_id: u32, - ) -> PaystackResult { - let url = format!("{}/transaction/{}", BASE_URL, transaction_id); - - match get_request(&self.api_key, &url, None).await { - Ok(response) => match response.status() { - StatusCode::OK => match response.json::().await { - Ok(content) => Ok(content), - Err(err) => Err(Error::Transaction(err.to_string())), - }, - _ => Err(Error::RequestNotSuccessful( - response.status().to_string(), - response.text().await?, - )), + Self { + api_key: key.to_string(), + transaction: Transaction { + api_key: key.to_string(), }, - Err(err) => Err(Error::FailedRequest(err.to_string())), - } - } - - /// All authorizations marked as reusable can be charged with this endpoint whenever you need to receive payments - /// - /// This function takes a Charge Struct as parameter - pub async fn charge_authorization( - &self, - charge: ChargeBody, - ) -> PaystackResult { - let url = format!("{}/transaction/charge_authorization", BASE_URL); - - match post_request(&self.api_key, &url, charge).await { - Ok(response) => match response.status() { - StatusCode::OK => match response.json::().await { - Ok(content) => Ok(content), - Err(err) => Err(Error::Charge(err.to_string())), - }, - _ => Err(Error::RequestNotSuccessful( - response.status().to_string(), - response.text().await?, - )), - }, - Err(err) => Err(Error::FailedRequest(err.to_string())), - } - } - - /// View the timeline of a transaction - /// - /// This method takes in the Transaction id or reference as a parameter - pub async fn view_transaction_timeline( - &self, - id: Option, - reference: Option, - ) -> PaystackResult { - // This is a hacky implementation to ensure that the transaction reference or id is not empty. - // If they are empty, a url without them as parameter is created. - let url: PaystackResult = match (id, reference) { - (Some(id), None) => Ok(format!("{}/transaction/timeline/{}", BASE_URL, id)), - (None, Some(reference)) => { - Ok(format!("{}/transaction/timeline/{}", BASE_URL, &reference)) - } - _ => { - return Err(Error::Transaction( - "Transaction Id or Reference is need to view transaction timeline".to_string(), - )) - } - }; - - let url = url.unwrap(); // Send the error back up the function - - match get_request(&self.api_key, &url, None).await { - Ok(response) => match response.status() { - StatusCode::OK => match response.json::().await { - Ok(content) => Ok(content), - Err(err) => Err(Error::Transaction(err.to_string())), - }, - _ => Err(Error::RequestNotSuccessful( - response.status().to_string(), - response.text().await?, - )), - }, - Err(err) => Err(Error::FailedRequest(err.to_string())), - } - } - - /// Total amount received on your account. - /// - /// This route normally takes a perPage or page query, - /// However in this case it is ignored. - /// If you need it in your work please open an issue - /// and it will be implemented. - pub async fn total_transactions(&self) -> PaystackResult { - let url = format!("{}/transaction/totals", BASE_URL); - - match get_request(&self.api_key, &url, None).await { - Ok(response) => match response.status() { - StatusCode::OK => match response.json::().await { - Ok(content) => Ok(content), - Err(err) => Err(Error::Transaction(err.to_string())), - }, - _ => Err(Error::RequestNotSuccessful( - response.status().to_string(), - response.text().await?, - )), - }, - Err(err) => Err(Error::FailedRequest(err.to_string())), - } - } - - /// Export a list of transactions carried out on your integration - /// - /// This method takes the following parameters - /// - Status (Optional): The status of the transactions to export. Defaults to all - /// - Currency (Optional): The currency of the transactions to export. Defaults to NGN - /// - Settled (Optional): To state of the transactions to export. Defaults to False. - pub async fn export_transaction( - &self, - status: Option, - currency: Option, - settled: Option, - ) -> PaystackResult { - let url = format!("{}/transaction/export", BASE_URL); - - // Specify a default option for settled transactions. - let settled = match settled { - Some(settled) => settled.to_string(), - None => String::from(""), - }; - - let query = vec![ - ("status", status.unwrap_or(Status::Success).to_string()), - ("currency", currency.unwrap_or(Currency::EMPTY).to_string()), - ("settled", settled), - ]; - - match get_request(&self.api_key, &url, Some(query)).await { - Ok(response) => match response.status() { - StatusCode::OK => match response.json::().await { - Ok(content) => Ok(content), - Err(err) => Err(Error::Transaction(err.to_string())), - }, - _ => Err(Error::RequestNotSuccessful( - response.status().to_string(), - response.text().await?, - )), - }, - Err(err) => Err(Error::FailedRequest(err.to_string())), - } - } - - /// Retrieve part of a payment from a customer. - /// - /// It takes a PartialDebitTransaction type as a parameter. - /// - /// NB: it must be created with the PartialDebitTransaction Builder. - pub async fn partial_debit( - &self, - transaction_body: PartialDebitTransactionBody, - ) -> PaystackResult { - let url = format!("{}/transaction/partial_debit", BASE_URL); - - match post_request(&self.api_key, &url, transaction_body).await { - Ok(response) => match response.status() { - StatusCode::OK => match response.json::().await { - Ok(content) => Ok(content), - Err(err) => Err(Error::Transaction(err.to_string())), - }, - _ => Err(Error::RequestNotSuccessful( - response.status().to_string(), - response.text().await?, - )), - }, - Err(err) => Err(Error::FailedRequest(err.to_string())), } } diff --git a/src/resources/transaction.rs b/src/resources/transaction.rs index dbbd01a..2538eeb 100644 --- a/src/resources/transaction.rs +++ b/src/resources/transaction.rs @@ -2,8 +2,13 @@ //! ============= //! TThe Transactions API allows you create and manage payments on your integration. -use crate::{Channel, Currency}; +use crate::{ + get_request, post_request, Channel, ChargeBody, Currency, Error, ExportTransactionResponse, + PaystackResult, Status, TransactionResponse, TransactionStatus, TransactionStatusList, + TransactionTimeline, TransactionTotalsResponse, +}; use derive_builder::Builder; +use reqwest::StatusCode; use serde::Serialize; /// This struct is used to create a transaction body for creating a transaction using the Paystack API. @@ -72,3 +77,271 @@ pub struct PartialDebitTransactionBody { #[builder(default = "None")] at_least: Option, } + +/// A Struct to hold all the functions of the transaction API route +#[derive(Debug, Clone)] +pub struct Transaction { + /// Paystack API Key + pub api_key: String, +} +static BASE_URL: &str = "https://api.paystack.co"; +impl Transaction { + /// This method initializes a new transaction using the Paystack API. + /// + /// It takes a Transaction type as its parameter + pub async fn initialize_transaction( + &self, + transaction_body: InitializeTransactionBody, + ) -> PaystackResult { + let url = format!("{}/transaction/initialize", BASE_URL); + + match post_request(&self.api_key, &url, transaction_body).await { + Ok(response) => match response.status() { + StatusCode::OK => match response.json::().await { + Ok(content) => Ok(content), + Err(err) => Err(Error::Transaction(err.to_string())), + }, + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), + }, + Err(err) => Err(Error::FailedRequest(err.to_string())), + } + } + + /// This method confirms the status of a transaction. + /// + /// It takes the following parameters: + /// - reference: The transaction reference used to initiate the transaction + pub async fn verify_transaction( + &self, + reference: &String, + ) -> PaystackResult { + let url = format!("{}/transaction/verify/{}", BASE_URL, reference); + + match get_request(&self.api_key, &url, None).await { + Ok(response) => match response.status() { + StatusCode::OK => match response.json::().await { + Ok(content) => Ok(content), + Err(err) => Err(Error::Transaction(err.to_string())), + }, + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), + }, + Err(err) => Err(Error::FailedRequest(err.to_string())), + } + } + + /// This method returns a Vec of transactions carried out on your integrations. + /// + /// The method takes the following parameters: + /// - perPage (Optional): Number of transactions to return. If None is passed as the parameter, the last 10 transactions are returned. + /// - status (Optional): Filter transactions by status, defaults to Success if no status is passed. + /// + pub async fn list_transactions( + &self, + number_of_transactions: Option, + status: Option, + ) -> PaystackResult { + let url = format!("{}/transaction", BASE_URL); + let query = vec![ + ("perPage", number_of_transactions.unwrap_or(10).to_string()), + ("status", status.unwrap_or(Status::Success).to_string()), + ]; + + match get_request(&self.api_key, &url, Some(query)).await { + Ok(response) => match response.status() { + StatusCode::OK => match response.json::().await { + Ok(content) => Ok(content), + Err(err) => Err(Error::Transaction(err.to_string())), + }, + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), + }, + Err(err) => Err(Error::FailedRequest(err.to_string())), + } + } + + /// Get details of a transaction carried out on your integration + /// + /// This methods take the Id of the desired transaction as a parameter + pub async fn fetch_transactions( + &self, + transaction_id: u32, + ) -> PaystackResult { + let url = format!("{}/transaction/{}", BASE_URL, transaction_id); + + match get_request(&self.api_key, &url, None).await { + Ok(response) => match response.status() { + StatusCode::OK => match response.json::().await { + Ok(content) => Ok(content), + Err(err) => Err(Error::Transaction(err.to_string())), + }, + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), + }, + Err(err) => Err(Error::FailedRequest(err.to_string())), + } + } + + /// All authorizations marked as reusable can be charged with this endpoint whenever you need to receive payments + /// + /// This function takes a Charge Struct as parameter + pub async fn charge_authorization( + &self, + charge: ChargeBody, + ) -> PaystackResult { + let url = format!("{}/transaction/charge_authorization", BASE_URL); + + match post_request(&self.api_key, &url, charge).await { + Ok(response) => match response.status() { + StatusCode::OK => match response.json::().await { + Ok(content) => Ok(content), + Err(err) => Err(Error::Charge(err.to_string())), + }, + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), + }, + Err(err) => Err(Error::FailedRequest(err.to_string())), + } + } + + /// View the timeline of a transaction + /// + /// This method takes in the Transaction id or reference as a parameter + pub async fn view_transaction_timeline( + &self, + id: Option, + reference: Option, + ) -> PaystackResult { + // This is a hacky implementation to ensure that the transaction reference or id is not empty. + // If they are empty, a url without them as parameter is created. + let url: PaystackResult = match (id, reference) { + (Some(id), None) => Ok(format!("{}/transaction/timeline/{}", BASE_URL, id)), + (None, Some(reference)) => { + Ok(format!("{}/transaction/timeline/{}", BASE_URL, &reference)) + } + _ => { + return Err(Error::Transaction( + "Transaction Id or Reference is need to view transaction timeline".to_string(), + )) + } + }; + + let url = url.unwrap(); // Send the error back up the function + + match get_request(&self.api_key, &url, None).await { + Ok(response) => match response.status() { + StatusCode::OK => match response.json::().await { + Ok(content) => Ok(content), + Err(err) => Err(Error::Transaction(err.to_string())), + }, + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), + }, + Err(err) => Err(Error::FailedRequest(err.to_string())), + } + } + + /// Total amount received on your account. + /// + /// This route normally takes a perPage or page query, + /// However in this case it is ignored. + /// If you need it in your work please open an issue + /// and it will be implemented. + pub async fn total_transactions(&self) -> PaystackResult { + let url = format!("{}/transaction/totals", BASE_URL); + + match get_request(&self.api_key, &url, None).await { + Ok(response) => match response.status() { + StatusCode::OK => match response.json::().await { + Ok(content) => Ok(content), + Err(err) => Err(Error::Transaction(err.to_string())), + }, + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), + }, + Err(err) => Err(Error::FailedRequest(err.to_string())), + } + } + + /// Export a list of transactions carried out on your integration + /// + /// This method takes the following parameters + /// - Status (Optional): The status of the transactions to export. Defaults to all + /// - Currency (Optional): The currency of the transactions to export. Defaults to NGN + /// - Settled (Optional): To state of the transactions to export. Defaults to False. + pub async fn export_transaction( + &self, + status: Option, + currency: Option, + settled: Option, + ) -> PaystackResult { + let url = format!("{}/transaction/export", BASE_URL); + + // Specify a default option for settled transactions. + let settled = match settled { + Some(settled) => settled.to_string(), + None => String::from(""), + }; + + let query = vec![ + ("status", status.unwrap_or(Status::Success).to_string()), + ("currency", currency.unwrap_or(Currency::EMPTY).to_string()), + ("settled", settled), + ]; + + match get_request(&self.api_key, &url, Some(query)).await { + Ok(response) => match response.status() { + StatusCode::OK => match response.json::().await { + Ok(content) => Ok(content), + Err(err) => Err(Error::Transaction(err.to_string())), + }, + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), + }, + Err(err) => Err(Error::FailedRequest(err.to_string())), + } + } + + /// Retrieve part of a payment from a customer. + /// + /// It takes a PartialDebitTransaction type as a parameter. + /// + /// NB: it must be created with the PartialDebitTransaction Builder. + pub async fn partial_debit( + &self, + transaction_body: PartialDebitTransactionBody, + ) -> PaystackResult { + let url = format!("{}/transaction/partial_debit", BASE_URL); + + match post_request(&self.api_key, &url, transaction_body).await { + Ok(response) => match response.status() { + StatusCode::OK => match response.json::().await { + Ok(content) => Ok(content), + Err(err) => Err(Error::Transaction(err.to_string())), + }, + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), + }, + Err(err) => Err(Error::FailedRequest(err.to_string())), + } + } +} diff --git a/src/utils.rs b/src/utils.rs index 8317368..5acc8f8 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,15 +2,15 @@ //! ============ //! This file contains utility sections that are used in different sections of the client -use std::fmt::Debug; -use reqwest::{Client, Response, Error}; +use reqwest::{Client, Error, Response}; use serde::Serialize; +use std::fmt::Debug; /// A function for sending GET request to a specified url /// with optional query parameters using reqwest client. pub async fn get_request( - api_key: &String, - url: &String, + api_key: &str, + url: &str, query: Option>, ) -> Result { let client = Client::new(); @@ -30,9 +30,9 @@ pub async fn get_request( /// A function for sending POST requests to a specified url /// using the reqwest client. -pub async fn post_request(api_key: &String, url: &String, body: T) -> Result - where - T: Debug + Serialize, +pub async fn post_request(api_key: &str, url: &str, body: T) -> Result +where + T: Debug + Serialize, { let client = Client::new(); let response = client @@ -51,9 +51,9 @@ pub async fn post_request(api_key: &String, url: &String, body: T) -> Result< /// A function for sending PUT requests to a specified url /// using the reqwest client. -pub async fn put_request(api_key: &String, url: &String, body: T) -> Result - where - T: Debug + Serialize, +pub async fn put_request(api_key: &str, url: &str, body: T) -> Result +where + T: Debug + Serialize, { let client = Client::new(); let response = client @@ -72,9 +72,9 @@ pub async fn put_request(api_key: &String, url: &String, body: T) -> Result(api_key: &String, url: &String, body: T) -> Result - where - T: Debug + Serialize, +pub async fn delete_request(api_key: &str, url: &str, body: T) -> Result +where + T: Debug + Serialize, { let client = Client::new(); let response = client @@ -89,4 +89,4 @@ pub async fn delete_request(api_key: &String, url: &String, body: T) -> Resul Ok(response) => Ok(response), Err(err) => Err(err), } -} \ No newline at end of file +} diff --git a/tests/api/transaction.rs b/tests/api/transaction.rs index fbda727..8f12945 100644 --- a/tests/api/transaction.rs +++ b/tests/api/transaction.rs @@ -29,6 +29,7 @@ async fn initialize_transaction_valid() { .unwrap(); // println!("{:#?}", &body); let res = client + .transaction .initialize_transaction(body) .await .expect("Unable to initialize transaction"); @@ -60,7 +61,7 @@ async fn initialize_transaction_fails_when_currency_is_not_supported_by_merchant .build() .unwrap(); - let res = client.initialize_transaction(body).await; + let res = client.transaction.initialize_transaction(body).await; // Assert match res { @@ -95,6 +96,7 @@ async fn valid_transaction_is_verified() { .unwrap(); let content = client + .transaction .initialize_transaction(body) .await .expect("unable to initiate transaction"); From bc21868a21d6e5ca47fd0964834975b1ab8fdfa0 Mon Sep 17 00:00:00 2001 From: Oghenemarho Orukele Date: Mon, 18 Sep 2023 09:05:58 +0200 Subject: [PATCH 6/9] fixed tests and example --- examples/transaction.rs | 2 ++ tests/api/charge.rs | 1 + tests/api/transaction.rs | 19 +++++++++++++++++-- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/examples/transaction.rs b/examples/transaction.rs index b5b1b55..dc9e054 100644 --- a/examples/transaction.rs +++ b/examples/transaction.rs @@ -48,6 +48,7 @@ async fn main() { // Verify transaction // Transaction reference can be a string or pulled out from the transaction response let transaction_status = client + .transaction .verify_transaction(&transaction.data.reference.to_string()) .await .expect("Unable to get transaction status"); @@ -62,6 +63,7 @@ async fn main() { // List of transactions let transactions = client + .transaction .list_transactions(Some(5), Some(Status::Success)) .await .expect("Unable to get all the transactions"); diff --git a/tests/api/charge.rs b/tests/api/charge.rs index ceb5290..71a188c 100644 --- a/tests/api/charge.rs +++ b/tests/api/charge.rs @@ -21,6 +21,7 @@ async fn charge_authorization_succeeds() { .unwrap(); let charge_response = client + .transaction .charge_authorization(charge) .await .expect("unable to authorize charge"); diff --git a/tests/api/transaction.rs b/tests/api/transaction.rs index 8f12945..a92c497 100644 --- a/tests/api/transaction.rs +++ b/tests/api/transaction.rs @@ -102,6 +102,7 @@ async fn valid_transaction_is_verified() { .expect("unable to initiate transaction"); let response = client + .transaction .verify_transaction(&content.data.reference) .await .expect("unable to verify transaction"); @@ -119,6 +120,7 @@ async fn list_specified_number_of_transactions_in_the_integration() { // Act let response = client + .transaction .list_transactions(Some(5), Some(Status::Abandoned)) .await .expect("unable to get list of integrated transactions"); @@ -136,6 +138,7 @@ async fn list_transactions_passes_with_default_values() { // Act let response = client + .transaction .list_transactions(None, None) .await .expect("unable to get list of integration transactions"); @@ -153,11 +156,13 @@ async fn fetch_transaction_succeeds() { // Act let response = client + .transaction .list_transactions(Some(1), Some(Status::Success)) .await .expect("unable to get list of integrated transactions"); let fetched_transaction = client + .transaction .fetch_transactions(response.data[0].id.unwrap()) .await .expect("unable to fetch transaction"); @@ -177,11 +182,13 @@ async fn view_transaction_timeline_passes_with_id() { // Act let response = client + .transaction .list_transactions(Some(1), Some(Status::Success)) .await .expect("unable to get list of integrated transactions"); let transaction_timeline = client + .transaction .view_transaction_timeline(response.data[0].id, None) .await .expect("unable to get transaction timeline"); @@ -198,6 +205,7 @@ async fn view_transaction_timeline_passes_with_reference() { // Act let response = client + .transaction .list_transactions(Some(1), Some(Status::Success)) .await .expect("unable to get list of integrated transactions"); @@ -205,6 +213,7 @@ async fn view_transaction_timeline_passes_with_reference() { // println!("{:#?}", response); let transaction_timeline = client + .transaction .view_transaction_timeline(None, response.data[0].reference.clone()) .await .expect("unable to get transaction timeline"); @@ -220,7 +229,10 @@ async fn view_transaction_timeline_fails_without_id_or_reference() { let client = get_paystack_client(); // Act - let res = client.view_transaction_timeline(None, None).await; + let res = client + .transaction + .view_transaction_timeline(None, None) + .await; // Assert match res { @@ -241,6 +253,7 @@ async fn get_transaction_total_is_successful() { // Act let res = client + .transaction .total_transactions() .await .expect("unable to get transaction total"); @@ -259,6 +272,7 @@ async fn export_transaction_succeeds_with_default_parameters() { // Act let res = client + .transaction .export_transaction(None, None, None) .await .expect("unable to export transactions"); @@ -276,6 +290,7 @@ async fn partial_debit_transaction_passes_or_fails_depending_on_merchant_status( // Act let transaction = client + .transaction .list_transactions(Some(1), Some(Status::Success)) .await .expect("Unable to get transaction list"); @@ -295,7 +310,7 @@ async fn partial_debit_transaction_passes_or_fails_depending_on_merchant_status( .build() .unwrap(); - let res = client.partial_debit(body).await; + let res = client.transaction.partial_debit(body).await; // Assert match res { From b103b539ad022937bff9d3dfd784971507d45f0c Mon Sep 17 00:00:00 2001 From: Oghenemarho Orukele Date: Mon, 18 Sep 2023 20:54:15 +0200 Subject: [PATCH 7/9] restructured code in client into seperate files --- src/client.rs | 216 ++--------------------------- src/resources/subaccount.rs | 45 +++++- src/resources/transaction.rs | 2 + src/resources/transaction_split.rs | 194 +++++++++++++++++++++++++- tests/api/subaccount.rs | 3 +- 5 files changed, 246 insertions(+), 214 deletions(-) diff --git a/src/client.rs b/src/client.rs index f3a8047..32b35f2 100644 --- a/src/client.rs +++ b/src/client.rs @@ -5,24 +5,19 @@ extern crate reqwest; extern crate serde_json; -use crate::{get_request, post_request, Transaction}; -use crate::{ - put_request, CreateSubAccountResponse, CreateSubaccountBody, CreateTransactionSplitBody, Error, - PaystackResult, ResponseWithoutData, Subaccount, TransactionSplitListResponse, - TransactionSplitResponse, UpdateTransactionSplitBody, -}; -use reqwest::StatusCode; +use crate::{Subaccount, Transaction, TransactionSplit}; use std::fmt::Debug; -static BASE_URL: &str = "https://api.paystack.co"; - /// This is the struct that allows you to authenticate to the PayStack API. /// It contains the API key which allows you to interact with the API. #[derive(Clone, Debug)] pub struct PaystackClient { - api_key: String, - /// Transaction API object + /// Transaction API route pub transaction: Transaction, + /// Transaction Split API route + pub transaction_split: TransactionSplit, + /// Subaccount API route + pub subaccount: Subaccount, } impl PaystackClient { @@ -32,206 +27,15 @@ impl PaystackClient { /// - key: Paystack API key. pub fn new(key: String) -> Self { Self { - api_key: key.to_string(), transaction: Transaction { api_key: key.to_string(), }, - } - } - - /// 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: CreateTransactionSplitBody, - ) -> PaystackResult { - let url = format!("{}/split", BASE_URL); - - match post_request(&self.api_key, &url, split_body).await { - Ok(response) => match response.status() { - StatusCode::OK => match response.json::().await { - Ok(content) => Ok(content), - Err(err) => Err(Error::TransactionSplit(err.to_string())), - }, - _ => Err(Error::RequestNotSuccessful( - response.status().to_string(), - response.text().await?, - )), - }, - Err(err) => Err(Error::FailedRequest(err.to_string())), - } - } - - /// 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, - split_active: Option, - ) -> PaystackResult { - 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 get_request(&self.api_key, &url, Some(query)).await { - Ok(response) => match response.status() { - StatusCode::OK => match response.json::().await { - Ok(content) => Ok(content), - Err(err) => Err(Error::TransactionSplit(err.to_string())), - }, - _ => Err(Error::RequestNotSuccessful( - response.status().to_string(), - response.text().await?, - )), - }, - Err(err) => Err(Error::FailedRequest(err.to_string())), - } - } - - /// 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 { - let url = format!("{}/split{}", BASE_URL, split_id); - - match get_request(&self.api_key, &url, None).await { - Ok(response) => match response.status() { - StatusCode::OK => match response.json::().await { - Ok(content) => Ok(content), - Err(err) => Err(Error::TransactionSplit(err.to_string())), - }, - _ => Err(Error::RequestNotSuccessful( - response.status().to_string(), - response.text().await?, - )), - }, - Err(err) => Err(Error::FailedRequest(err.to_string())), - } - } - - /// 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: UpdateTransactionSplitBody, - ) -> PaystackResult { - let url = format!("{}/split/{}", BASE_URL, split_id); - - match put_request(&self.api_key, &url, body).await { - Ok(response) => match response.status() { - StatusCode::OK => match response.json::().await { - Ok(content) => Ok(content), - Err(err) => Err(Error::TransactionSplit(err.to_string())), - }, - _ => Err(Error::RequestNotSuccessful( - response.status().to_string(), - response.text().await?, - )), - }, - Err(err) => Err(Error::FailedRequest(err.to_string())), - } - } - - /// 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`: Subaccount to add to the transaction split. - pub async fn add_or_update_subaccount_split( - &self, - split_id: String, - body: Subaccount, - ) -> PaystackResult { - let url = format!("{}/split/{}/subaccount/add", BASE_URL, split_id); - - match post_request(&self.api_key, &url, body).await { - Ok(response) => match response.status() { - StatusCode::OK => match response.json::().await { - Ok(content) => Ok(content), - Err(err) => Err(Error::TransactionSplit(err.to_string())), - }, - _ => Err(Error::RequestNotSuccessful( - response.status().to_string(), - response.text().await?, - )), - }, - Err(err) => Err(Error::FailedRequest(err.to_string())), - } - } - - /// 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 { - let url = format!("{}/split/{}/subaccount/remove", BASE_URL, split_id); - - match post_request(&self.api_key, &url, subaccount).await { - Ok(response) => match response.status() { - StatusCode::OK => match response.json::().await { - Ok(content) => Ok(content), - Err(err) => Err(Error::TransactionSplit(err.to_string())), - }, - _ => Err(Error::RequestNotSuccessful( - response.status().to_string(), - response.text().await?, - )), + transaction_split: TransactionSplit { + api_key: key.to_string(), }, - Err(err) => Err(Error::FailedRequest(err.to_string())), - } - } - - /// Creates a new subaccount. - /// - /// Takes in the following parameters - /// - body: subaccount to create. - pub async fn create_subaccount( - &self, - body: CreateSubaccountBody, - ) -> PaystackResult { - let url = format!("{}/subaccount", BASE_URL); - - match post_request(&self.api_key, &url, body).await { - Ok(response) => match response.status() { - StatusCode::CREATED => match response.json::().await { - Ok(content) => Ok(content), - Err(err) => Err(Error::Subaccount(err.to_string())), - }, - _ => Err(Error::RequestNotSuccessful( - response.status().to_string(), - response.text().await?, - )), + subaccount: Subaccount { + api_key: key.to_string(), }, - Err(err) => Err(Error::FailedRequest(err.to_string())), } } } diff --git a/src/resources/subaccount.rs b/src/resources/subaccount.rs index 6abb552..e54a6fe 100644 --- a/src/resources/subaccount.rs +++ b/src/resources/subaccount.rs @@ -3,8 +3,11 @@ //! The Subaccounts API allows you create and manage subaccounts on your integration. //! Subaccounts can be used to split payment between two accounts (your main account and a sub account). -use serde::Serialize; use derive_builder::Builder; +use reqwest::StatusCode; +use serde::Serialize; + +use crate::{post_request, CreateSubAccountResponse, Error, PaystackResult}; /// This struct is used to create the body for creating a subaccount on your integration. #[derive(Serialize, Debug, Builder)] @@ -34,5 +37,41 @@ pub struct CreateSubaccountBody { /// added to your transaction when displayed on the dashboard. /// Sample: {"custom_fields":[{"display_name":"Cart ID","variable_name": "cart_id","value": "8393"}]} #[builder(default = "None")] - metadata: Option -} \ No newline at end of file + metadata: Option, +} + +/// A struct to hold all functions in the subaccount API route +#[derive(Debug, Clone)] +pub struct Subaccount { + /// Paystack API key + pub api_key: String, +} + +static BASE_URL: &str = "https://api.paystack.co"; + +impl Subaccount { + /// Creates a new subaccount. + /// + /// Takes in the following parameters + /// - body: subaccount to create. + pub async fn create_subaccount( + &self, + body: CreateSubaccountBody, + ) -> PaystackResult { + let url = format!("{}/subaccount", BASE_URL); + + match post_request(&self.api_key, &url, body).await { + Ok(response) => match response.status() { + StatusCode::CREATED => match response.json::().await { + Ok(content) => Ok(content), + Err(err) => Err(Error::Subaccount(err.to_string())), + }, + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), + }, + Err(err) => Err(Error::FailedRequest(err.to_string())), + } + } +} diff --git a/src/resources/transaction.rs b/src/resources/transaction.rs index 2538eeb..c86b5bd 100644 --- a/src/resources/transaction.rs +++ b/src/resources/transaction.rs @@ -84,7 +84,9 @@ pub struct Transaction { /// Paystack API Key pub api_key: String, } + static BASE_URL: &str = "https://api.paystack.co"; + impl Transaction { /// This method initializes a new transaction using the Paystack API. /// diff --git a/src/resources/transaction_split.rs b/src/resources/transaction_split.rs index 0ad36fc..1038f23 100644 --- a/src/resources/transaction_split.rs +++ b/src/resources/transaction_split.rs @@ -3,8 +3,12 @@ //! The Transaction Splits API enables merchants split the settlement for a transaction //! across their payout account, and one or more subaccounts. -use crate::{BearerType, Currency, SplitType}; +use crate::{ + get_request, post_request, put_request, BearerType, Currency, Error, PaystackResult, + ResponseWithoutData, SplitType, TransactionSplitListResponse, TransactionSplitResponse, +}; use derive_builder::Builder; +use reqwest::StatusCode; use serde::Serialize; /// This struct is used to create a split payment on your integration. @@ -19,7 +23,7 @@ pub struct CreateTransactionSplitBody { /// Any of the supported currency currency: Currency, /// A list of object containing subaccount code and number of shares: `[{subaccount: ‘ACT_xxxxxxxxxx’, share: xxx},{...}]` - subaccounts: Vec, + subaccounts: Vec, /// Any of subaccount bearer_type: BearerType, /// Subaccount code @@ -31,7 +35,7 @@ pub struct CreateTransactionSplitBody { /// It is also possible to extract a single field from this struct to use as well. /// The Struct is constructed using the `SubaccountBuilder` #[derive(Serialize, Debug, Clone, Builder)] -pub struct Subaccount { +pub struct SubaccountBody { /// This is the sub account code pub subaccount: String, /// This is the transaction share for the subaccount @@ -51,5 +55,187 @@ pub struct UpdateTransactionSplitBody { pub bearer_type: Option, /// Subaccount code of a subaccount in the split group. This should be specified only if the `bearer_type is subaccount #[builder(default = "None")] - pub bearer_subaccount: Option, + pub bearer_subaccount: Option, +} + +/// A struct to hold all the functions of the transaction split API route +#[derive(Debug, Clone)] +pub struct TransactionSplit { + /// Paystack API key + pub api_key: String, +} + +static BASE_URL: &str = "https://api.paystack.co"; + +impl TransactionSplit { + /// 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: CreateTransactionSplitBody, + ) -> PaystackResult { + let url = format!("{}/split", BASE_URL); + + match post_request(&self.api_key, &url, split_body).await { + Ok(response) => match response.status() { + StatusCode::OK => match response.json::().await { + Ok(content) => Ok(content), + Err(err) => Err(Error::TransactionSplit(err.to_string())), + }, + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), + }, + Err(err) => Err(Error::FailedRequest(err.to_string())), + } + } + + /// 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, + split_active: Option, + ) -> PaystackResult { + 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 get_request(&self.api_key, &url, Some(query)).await { + Ok(response) => match response.status() { + StatusCode::OK => match response.json::().await { + Ok(content) => Ok(content), + Err(err) => Err(Error::TransactionSplit(err.to_string())), + }, + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), + }, + Err(err) => Err(Error::FailedRequest(err.to_string())), + } + } + + /// 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 { + let url = format!("{}/split{}", BASE_URL, split_id); + + match get_request(&self.api_key, &url, None).await { + Ok(response) => match response.status() { + StatusCode::OK => match response.json::().await { + Ok(content) => Ok(content), + Err(err) => Err(Error::TransactionSplit(err.to_string())), + }, + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), + }, + Err(err) => Err(Error::FailedRequest(err.to_string())), + } + } + + /// 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: UpdateTransactionSplitBody, + ) -> PaystackResult { + let url = format!("{}/split/{}", BASE_URL, split_id); + + match put_request(&self.api_key, &url, body).await { + Ok(response) => match response.status() { + StatusCode::OK => match response.json::().await { + Ok(content) => Ok(content), + Err(err) => Err(Error::TransactionSplit(err.to_string())), + }, + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), + }, + Err(err) => Err(Error::FailedRequest(err.to_string())), + } + } + + /// 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`: Subaccount to add to the transaction split. + pub async fn add_or_update_subaccount_split( + &self, + split_id: String, + body: SubaccountBody, + ) -> PaystackResult { + let url = format!("{}/split/{}/subaccount/add", BASE_URL, split_id); + + match post_request(&self.api_key, &url, body).await { + Ok(response) => match response.status() { + StatusCode::OK => match response.json::().await { + Ok(content) => Ok(content), + Err(err) => Err(Error::TransactionSplit(err.to_string())), + }, + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), + }, + Err(err) => Err(Error::FailedRequest(err.to_string())), + } + } + + /// 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 { + let url = format!("{}/split/{}/subaccount/remove", BASE_URL, split_id); + + match post_request(&self.api_key, &url, subaccount).await { + Ok(response) => match response.status() { + StatusCode::OK => match response.json::().await { + Ok(content) => Ok(content), + Err(err) => Err(Error::TransactionSplit(err.to_string())), + }, + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), + }, + Err(err) => Err(Error::FailedRequest(err.to_string())), + } + } } diff --git a/tests/api/subaccount.rs b/tests/api/subaccount.rs index 6e059c5..1c3797f 100644 --- a/tests/api/subaccount.rs +++ b/tests/api/subaccount.rs @@ -26,6 +26,7 @@ async fn create_subaccount_passes_with_valid_data() { // println!("{:#?}", body); let res = client + .subaccount .create_subaccount(body) .await .expect("Unable to Create a subaccount"); @@ -51,7 +52,7 @@ async fn create_subaccount_fails_with_invalid_data() { .build() .unwrap(); - let res = client.create_subaccount(body).await; + let res = client.subaccount.create_subaccount(body).await; // Assert match res { From 1832120063c88617fb02dd31326dc53c74a20b3f Mon Sep 17 00:00:00 2001 From: Oghenemarho Orukele Date: Mon, 18 Sep 2023 23:42:19 +0200 Subject: [PATCH 8/9] implemented fetch subaccount route --- README.md | 1 + src/client.rs | 18 ++----- src/lib.rs | 1 + src/resources/subaccount.rs | 81 ++++++++++++++++++++++++++++-- src/resources/transaction.rs | 21 +++++--- src/resources/transaction_split.rs | 7 ++- src/response.rs | 79 +++++++++++++++++++++-------- tests/api/subaccount.rs | 55 ++++++++++++++++++++ 8 files changed, 214 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 9b1a7fc..c0d7616 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,7 @@ Initializing an instance of the Paystack client and creating a transaction. .unwrap(); let transaction = client + .transaction .initialize_transaction(body) .await .expect("Unable to create transaction"); diff --git a/src/client.rs b/src/client.rs index 32b35f2..603e817 100644 --- a/src/client.rs +++ b/src/client.rs @@ -1,16 +1,10 @@ //! Client //! =========== //! The file for the Paystack API client and it's associated functions - -extern crate reqwest; -extern crate serde_json; - use crate::{Subaccount, Transaction, TransactionSplit}; -use std::fmt::Debug; /// This is the struct that allows you to authenticate to the PayStack API. /// It contains the API key which allows you to interact with the API. -#[derive(Clone, Debug)] pub struct PaystackClient { /// Transaction API route pub transaction: Transaction, @@ -27,15 +21,9 @@ impl PaystackClient { /// - key: Paystack API key. pub fn new(key: String) -> Self { Self { - transaction: Transaction { - api_key: key.to_string(), - }, - transaction_split: TransactionSplit { - api_key: key.to_string(), - }, - subaccount: Subaccount { - api_key: key.to_string(), - }, + transaction: Transaction::new(key.to_string()), + transaction_split: TransactionSplit::new(key.to_string()), + subaccount: Subaccount::new(key.to_string()), } } } diff --git a/src/lib.rs b/src/lib.rs index 34c33e5..dd20c63 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,6 +43,7 @@ //! .unwrap(); //! //! let transaction = client +//! .transaction //! .initialize_transaction(body) //! .await //! .expect("Unable to create transaction"); diff --git a/src/resources/subaccount.rs b/src/resources/subaccount.rs index e54a6fe..f86bcde 100644 --- a/src/resources/subaccount.rs +++ b/src/resources/subaccount.rs @@ -7,7 +7,10 @@ use derive_builder::Builder; use reqwest::StatusCode; use serde::Serialize; -use crate::{post_request, CreateSubAccountResponse, Error, PaystackResult}; +use crate::{ + get_request, post_request, CreateSubaccountResponse, Error, FetchSubaccountResponse, + ListSubaccountsResponse, PaystackResult, +}; /// This struct is used to create the body for creating a subaccount on your integration. #[derive(Serialize, Debug, Builder)] @@ -44,25 +47,93 @@ pub struct CreateSubaccountBody { #[derive(Debug, Clone)] pub struct Subaccount { /// Paystack API key - pub api_key: String, + api_key: String, } static BASE_URL: &str = "https://api.paystack.co"; impl Subaccount { - /// Creates a new subaccount. + /// Constructor for the subaccount object + pub fn new(key: String) -> Self { + Subaccount { api_key: key } + } + + /// Create a subaccount on your integration /// /// Takes in the following parameters /// - body: subaccount to create. pub async fn create_subaccount( &self, body: CreateSubaccountBody, - ) -> PaystackResult { + ) -> PaystackResult { let url = format!("{}/subaccount", BASE_URL); match post_request(&self.api_key, &url, body).await { Ok(response) => match response.status() { - StatusCode::CREATED => match response.json::().await { + StatusCode::CREATED => match response.json::().await { + Ok(content) => Ok(content), + Err(err) => Err(Error::Subaccount(err.to_string())), + }, + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), + }, + Err(err) => Err(Error::FailedRequest(err.to_string())), + } + } + + /// List subaccounts available on your integration + /// + /// Take in the following parameters + /// - perPage: Specify how many records you want to retrieve per page. If not specify we use a default value of 50. + /// - page: Specify exactly what page you want to retrieve. If not specify we use a default value of 1. + /// - from: A timestamp from which to start listing subaccounts e.g. `2016-09-24T00:00:05.000Z`, `2016-09-21` + /// - to: A timestamp at which to stop listing subaccounts e.g. `2016-09-24T00:00:05.000Z`, `2016-09-21` + pub async fn list_subaccounts( + &self, + per_page: Option, + page: Option, + from: Option, + to: Option, + ) -> PaystackResult { + let url = format!("{}/subaccount", BASE_URL); + + let query = vec![ + ("perPage", per_page.unwrap_or(50).to_string()), + ("page", page.unwrap_or(1).to_string()), + ("from", from.unwrap_or_default()), + ("to", to.unwrap_or_default()), + ]; + + match get_request(&self.api_key, &url, Some(query)).await { + Ok(response) => match response.status() { + StatusCode::OK => match response.json::().await { + Ok(content) => Ok(content), + Err(err) => Err(Error::Subaccount(err.to_string())), + }, + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), + }, + Err(err) => Err(Error::FailedRequest(err.to_string())), + } + } + + /// Get details of a subaccount on your integration. + /// + /// Takes the following parameters: + /// - id_or_code: The subaccount `ID` or `code` you want to fetch + pub async fn fetch_subaccount( + &self, + id_or_code: String, + ) -> PaystackResult { + let url = format!("{}/subaccount/{}", BASE_URL, id_or_code); + + match get_request(&self.api_key, &url, None).await { + Ok(response) => match response.status() { + StatusCode::OK => match response.json::().await { Ok(content) => Ok(content), Err(err) => Err(Error::Subaccount(err.to_string())), }, diff --git a/src/resources/transaction.rs b/src/resources/transaction.rs index c86b5bd..878e06e 100644 --- a/src/resources/transaction.rs +++ b/src/resources/transaction.rs @@ -82,13 +82,18 @@ pub struct PartialDebitTransactionBody { #[derive(Debug, Clone)] pub struct Transaction { /// Paystack API Key - pub api_key: String, + api_key: String, } static BASE_URL: &str = "https://api.paystack.co"; impl Transaction { - /// This method initializes a new transaction using the Paystack API. + /// Constructor for the transaction object + pub fn new(key: String) -> Self { + Transaction { api_key: key } + } + + /// Initialize a transaction from your backend. /// /// It takes a Transaction type as its parameter pub async fn initialize_transaction( @@ -112,7 +117,7 @@ impl Transaction { } } - /// This method confirms the status of a transaction. + /// Confirm the status of a transaction. /// /// It takes the following parameters: /// - reference: The transaction reference used to initiate the transaction @@ -137,7 +142,7 @@ impl Transaction { } } - /// This method returns a Vec of transactions carried out on your integrations. + /// List transactions carried out on your integration. /// /// The method takes the following parameters: /// - perPage (Optional): Number of transactions to return. If None is passed as the parameter, the last 10 transactions are returned. @@ -169,7 +174,7 @@ impl Transaction { } } - /// Get details of a transaction carried out on your integration + /// Get details of a transaction carried out on your integration. /// /// This methods take the Id of the desired transaction as a parameter pub async fn fetch_transactions( @@ -193,7 +198,7 @@ impl Transaction { } } - /// All authorizations marked as reusable can be charged with this endpoint whenever you need to receive payments + /// All authorizations marked as reusable can be charged with this endpoint whenever you need to receive payments. /// /// This function takes a Charge Struct as parameter pub async fn charge_authorization( @@ -217,7 +222,7 @@ impl Transaction { } } - /// View the timeline of a transaction + /// View the timeline of a transaction. /// /// This method takes in the Transaction id or reference as a parameter pub async fn view_transaction_timeline( @@ -280,7 +285,7 @@ impl Transaction { } } - /// Export a list of transactions carried out on your integration + /// Export a list of transactions carried out on your integration. /// /// This method takes the following parameters /// - Status (Optional): The status of the transactions to export. Defaults to all diff --git a/src/resources/transaction_split.rs b/src/resources/transaction_split.rs index 1038f23..b1f1667 100644 --- a/src/resources/transaction_split.rs +++ b/src/resources/transaction_split.rs @@ -62,12 +62,17 @@ pub struct UpdateTransactionSplitBody { #[derive(Debug, Clone)] pub struct TransactionSplit { /// Paystack API key - pub api_key: String, + api_key: String, } static BASE_URL: &str = "https://api.paystack.co"; impl TransactionSplit { + /// Constructor for the Transaction Split object + pub fn new(key: String) -> Self { + TransactionSplit { api_key: key } + } + /// Create a split payment on your integration. /// /// This method takes a TransactionSplit object as a parameter. diff --git a/src/response.rs b/src/response.rs index 09d1b88..0d19123 100644 --- a/src/response.rs +++ b/src/response.rs @@ -40,7 +40,7 @@ pub struct TransactionStatus { } /// This struct represents a list of transaction status. -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] pub struct TransactionStatusList { /// This lets you know if your request was successful or not. pub status: bool, @@ -49,10 +49,12 @@ pub struct TransactionStatusList { /// This contains the results of your request. /// In this case, it is a vector of objects. pub data: Vec, + /// The meta key is used to provide context for the contents of the data key. + pub meta: MetaData, } /// This struct represents the transaction timeline. -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] pub struct TransactionTimeline { /// This lets you know if your request was successful or not. pub status: bool, @@ -63,7 +65,7 @@ pub struct TransactionTimeline { } /// This struct represents the transaction timeline data. -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] pub struct TransactionTimelineData { /// Time spent in carrying out the transaction in ms. pub time_spent: Option, @@ -86,7 +88,7 @@ pub struct TransactionTimelineData { } /// This struct represents the transaction history data -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] pub struct TransactionHistory { /// Transaction action. #[serde(rename = "type")] @@ -98,7 +100,7 @@ pub struct TransactionHistory { } /// This struct represents the data of the transaction status response. -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Serialize, Debug, Clone)] pub struct TransactionStatusData { /// Id of the Transaction pub id: Option, @@ -133,7 +135,7 @@ pub struct TransactionStatusData { } /// This struct represents the authorization data of the transaction status response -#[derive(Debug, Deserialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct Authorization { /// Authorization code generated for the Transaction. pub authorization_code: Option, @@ -187,7 +189,7 @@ pub struct Customer { } /// Represents the response of the total amount received on your account -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct TransactionTotalsResponse { /// This lets you know if your request was successful or not. pub status: bool, @@ -198,7 +200,7 @@ pub struct TransactionTotalsResponse { } /// Transaction total data. -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct TransactionTotalData { /// Total number of transactions in the integration. pub total_transactions: Option, @@ -215,7 +217,7 @@ pub struct TransactionTotalData { } /// Transaction volume by currency. -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct VolumeByCurrency { /// Currency code. pub currency: String, @@ -242,7 +244,7 @@ pub struct ExportTransactionData { } /// Represents the response of the partial debit transaction. -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct PartialDebitTransactionResponse { /// This lets you know if your request was successful or not. pub status: bool, @@ -301,14 +303,27 @@ pub struct SplitData { #[derive(Debug, Deserialize, Serialize)] pub struct SubaccountData { /// Sub account data - pub subaccount: SubaccountResponse, + pub subaccount: SubaccountsResponseData, /// Share of split assigned to this sub pub share: u32, } -/// Represents a subaccount in the percentage split data. +/// Response from List Subaccount route #[derive(Debug, Deserialize, Serialize)] -pub struct SubaccountResponse { +pub struct ListSubaccountsResponse { + /// This lets you know if your request was successful or not. + pub status: bool, + /// This is a summary of the response and its status. + pub message: String, + /// This contain the results of your request. + pub data: Vec, + /// The meta key is used to provide context for the contents of the data key. + pub meta: MetaData, +} + +/// Data of the list Subaccount response +#[derive(Debug, Deserialize, Serialize)] +pub struct SubaccountsResponseData { /// Integration Id of subaccount. pub integration: Option, /// Subaccount domain. @@ -337,10 +352,6 @@ pub struct SubaccountResponse { pub account_number: String, /// Settlement schedule of subaccount. pub settlement_schedule: Option, - /// Status of subaccount. - pub active: Option, - /// Migrate subaccount or not. - pub migrate: Option, /// The ID of the subaccount. pub id: u32, /// Creation time of subaccount. @@ -351,6 +362,23 @@ pub struct SubaccountResponse { pub updated_at: Option, } +/// MetaData of list response +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct MetaData { + /// This is the total number of transactions that were performed by the customer. + pub total: u32, + /// This is the number of records skipped before the first record in the array returned. + pub skipped: u32, + /// This is the maximum number of records that will be returned per request. + #[serde(rename = "perPage")] + pub per_page: String, + /// This is the current `page` being returned. + pub page: u32, + /// This is how many pages in total are available for retrieval considering the maximum records per page specified. + #[serde(rename = "pageCount")] + pub page_count: u32, +} + /// Represents the JSON response containing percentage split information. #[derive(Debug, Deserialize, Serialize)] pub struct TransactionSplitListResponse { @@ -373,11 +401,22 @@ pub struct ResponseWithoutData { /// Represents the JSON response for subaccount creation. #[derive(Debug, Deserialize, Serialize)] -pub struct CreateSubAccountResponse { +pub struct CreateSubaccountResponse { /// The status of the JSON response. pub status: bool, /// The message associated with the JSON response pub message: String, /// Subaccount response data - pub data: SubaccountResponse, -} \ No newline at end of file + pub data: SubaccountsResponseData, +} + +/// Represents the JSON response for fetch subaccount. +#[derive(Debug, Deserialize, Serialize)] +pub struct FetchSubaccountResponse { + /// The status of the JSON response. + pub status: bool, + /// The message associated with the JSON response. + pub message: String, + /// Fetch Subaccount response data. + pub data: SubaccountsResponseData, +} diff --git a/tests/api/subaccount.rs b/tests/api/subaccount.rs index 1c3797f..2a2eabb 100644 --- a/tests/api/subaccount.rs +++ b/tests/api/subaccount.rs @@ -65,3 +65,58 @@ async fn create_subaccount_fails_with_invalid_data() { } } } + +#[tokio::test] +async fn list_subaccounts_returns_a_valid_payload() { + // Arrange + let client = get_paystack_client(); + + // Act + let res = client + .subaccount + .list_subaccounts(Some(10), Some(1), None, None) + .await + .expect("Unable to list subaccounts"); + + // Assert + assert!(res.status); + assert!(!res.data.is_empty()); + assert_eq!(res.message, "Subaccounts retrieved"); +} + +#[tokio::test] +async fn fetch_subaccount_with_id_returns_a_valid_payload() { + // Arrange + let client = get_paystack_client(); + let business_name: String = CompanyName().fake(); + let description: String = Sentence(5..10).fake(); + let (account_number, bank_code) = get_bank_account_number_and_code(); + + // Act + let body = CreateSubaccountBodyBuilder::default() + .business_name(business_name) + .settlement_bank(bank_code.clone()) + .account_number(account_number.clone()) + .percentage_charge(18.2) + .description(description) + .build() + .unwrap(); + + // println!("{:#?}", body); + let subaccount = client + .subaccount + .create_subaccount(body) + .await + .expect("Unable to Create a subaccount"); + + let res = client + .subaccount + .fetch_subaccount(subaccount.data.id.to_string()) + .await + .expect("Unable to fetch subaccount"); + + // Assert + assert!(res.status); + assert_eq!(res.message, "Subaccount retrieved"); + assert_eq!(res.data.account_number, subaccount.data.account_number); +} From fcc000ad79a286e56d992f5c97b831fab5d4c3da Mon Sep 17 00:00:00 2001 From: Oghenemarho Orukele Date: Tue, 19 Sep 2023 00:02:45 +0200 Subject: [PATCH 9/9] implemented and tested update subaccount route --- README.md | 2 +- src/resources/subaccount.rs | 31 ++++++++- tests/api/subaccount.rs | 131 ++++++++++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c0d7616..707749f 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ The client currently covers the following section of the API, and the sections t - [ ] Customers - [ ] Dedicated Virtual Account - [ ] Apple Pay -- [ ] Subaccounts +- [x] Subaccounts - [ ] Plans - [ ] Subscriptions - [ ] Transfer Recipients diff --git a/src/resources/subaccount.rs b/src/resources/subaccount.rs index f86bcde..0982bbf 100644 --- a/src/resources/subaccount.rs +++ b/src/resources/subaccount.rs @@ -8,8 +8,8 @@ use reqwest::StatusCode; use serde::Serialize; use crate::{ - get_request, post_request, CreateSubaccountResponse, Error, FetchSubaccountResponse, - ListSubaccountsResponse, PaystackResult, + get_request, post_request, put_request, CreateSubaccountResponse, Error, + FetchSubaccountResponse, ListSubaccountsResponse, PaystackResult, }; /// This struct is used to create the body for creating a subaccount on your integration. @@ -145,4 +145,31 @@ impl Subaccount { Err(err) => Err(Error::FailedRequest(err.to_string())), } } + + /// Update a subaccount details on your integration. + /// + /// Takes the following parameters: + /// - id_or_code: Subaccount's ID or code. + /// - body: Subaccount modification payload + pub async fn update_subaccount( + &self, + id_or_code: String, + body: CreateSubaccountBody, + ) -> PaystackResult { + let url = format!("{}/subaccount/{}", BASE_URL, id_or_code); + + match put_request(&self.api_key, &url, body).await { + Ok(response) => match response.status() { + StatusCode::OK => match response.json::().await { + Ok(content) => Ok(content), + Err(err) => Err(Error::Subaccount(err.to_string())), + }, + _ => Err(Error::RequestNotSuccessful( + response.status().to_string(), + response.text().await?, + )), + }, + Err(err) => Err(Error::FailedRequest(err.to_string())), + } + } } diff --git a/tests/api/subaccount.rs b/tests/api/subaccount.rs index 2a2eabb..30ff5d5 100644 --- a/tests/api/subaccount.rs +++ b/tests/api/subaccount.rs @@ -120,3 +120,134 @@ async fn fetch_subaccount_with_id_returns_a_valid_payload() { assert_eq!(res.message, "Subaccount retrieved"); assert_eq!(res.data.account_number, subaccount.data.account_number); } + +#[tokio::test] +async fn fetch_subaccount_with_subaccount_code_returns_a_valid_payload() { + // Arrange + let client = get_paystack_client(); + let business_name: String = CompanyName().fake(); + let description: String = Sentence(5..10).fake(); + let (account_number, bank_code) = get_bank_account_number_and_code(); + + // Act + let body = CreateSubaccountBodyBuilder::default() + .business_name(business_name) + .settlement_bank(bank_code.clone()) + .account_number(account_number.clone()) + .percentage_charge(18.2) + .description(description) + .build() + .unwrap(); + + // println!("{:#?}", body); + let subaccount = client + .subaccount + .create_subaccount(body) + .await + .expect("Unable to Create a subaccount"); + + let res = client + .subaccount + .fetch_subaccount(subaccount.data.subaccount_code) + .await + .expect("Unable to fetch subaccount"); + + // Assert + assert!(res.status); + assert_eq!(res.message, "Subaccount retrieved"); + assert_eq!(res.data.account_number, subaccount.data.account_number); +} + +#[tokio::test] +async fn modify_subaccount_with_subaccount_id_returns_a_valid_payload() { + // Arrange + let client = get_paystack_client(); + let business_name: String = CompanyName().fake(); + let description: String = Sentence(5..10).fake(); + let (account_number, bank_code) = get_bank_account_number_and_code(); + + // Act + let body = CreateSubaccountBodyBuilder::default() + .business_name(business_name) + .settlement_bank(bank_code.clone()) + .account_number(account_number.clone()) + .percentage_charge(18.2) + .description(description) + .build() + .unwrap(); + + // println!("{:#?}", body); + let subaccount = client + .subaccount + .create_subaccount(body) + .await + .expect("Unable to Create a subaccount"); + + let new_body = CreateSubaccountBodyBuilder::default() + .business_name("New Business Name".to_string()) + .settlement_bank(bank_code.clone()) + .account_number(account_number.clone()) + .percentage_charge(18.2) + .description("This should be modified".to_string()) + .build() + .unwrap(); + + let res = client + .subaccount + .update_subaccount(subaccount.data.subaccount_code, new_body) + .await + .expect("Unable to fetch subaccount"); + + // Assert + assert!(res.status); + assert_eq!(res.message, "Subaccount updated"); + assert_eq!(res.data.description.unwrap(), "This should be modified"); + assert_eq!(res.data.business_name, "New Business Name") +} + +#[tokio::test] +async fn modify_subaccount_with_subaccount_code_returns_a_valid_payload() { + // Arrange + let client = get_paystack_client(); + let business_name: String = CompanyName().fake(); + let description: String = Sentence(5..10).fake(); + let (account_number, bank_code) = get_bank_account_number_and_code(); + + // Act + let body = CreateSubaccountBodyBuilder::default() + .business_name(business_name) + .settlement_bank(bank_code.clone()) + .account_number(account_number.clone()) + .percentage_charge(18.2) + .description(description) + .build() + .unwrap(); + + // println!("{:#?}", body); + let subaccount = client + .subaccount + .create_subaccount(body) + .await + .expect("Unable to Create a subaccount"); + + let new_body = CreateSubaccountBodyBuilder::default() + .business_name("New Business Name".to_string()) + .settlement_bank(bank_code.clone()) + .account_number(account_number.clone()) + .percentage_charge(18.2) + .description("This should be modified".to_string()) + .build() + .unwrap(); + + let res = client + .subaccount + .update_subaccount(subaccount.data.id.to_string(), new_body) + .await + .expect("Unable to fetch subaccount"); + + // Assert + assert!(res.status); + assert_eq!(res.message, "Subaccount updated"); + assert_eq!(res.data.description.unwrap(), "This should be modified"); + assert_eq!(res.data.business_name, "New Business Name") +}