From dc14ebe733a750576d9c0140ab97254c04f61aa9 Mon Sep 17 00:00:00 2001 From: Oghenemarho Orukele Date: Tue, 4 Jul 2023 21:04:39 +0200 Subject: [PATCH] added charge authorization function --- CONTRIBUTING.md | 4 +- src/client.rs | 59 ++++++++++++++++++- src/error.rs | 3 + src/lib.rs | 2 +- src/request.rs | 120 ++++++++++++++++++++++++++++++++++++++- src/response.rs | 21 +++---- tests/api/transaction.rs | 90 ++++++++++++++++++++++++++++- 7 files changed, 282 insertions(+), 17 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 67aceaa..9f9a396 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,4 +2,6 @@ Still working on a defined process for contributing, feel free to create a pull request. I will review it and merge if it passes the CI pipeline. -Feel free to contribute to this contribution fill too! +If you make any change to the current code, please make sure all test pass. + +Feel free to contribute to this contribution instruction too! diff --git a/src/client.rs b/src/client.rs index b1f91fa..df406fa 100644 --- a/src/client.rs +++ b/src/client.rs @@ -6,8 +6,8 @@ extern crate reqwest; extern crate serde_json; use crate::{ - PaystackResult, RequestNotSuccessful, Transaction, TransactionResponse, TransactionStatus, - TransactionStatusList, + Charge, PaystackResult, RequestNotSuccessful, Transaction, TransactionResponse, + TransactionStatus, TransactionStatusList, }; static BASE_URL: &str = "https://api.paystack.co"; @@ -87,7 +87,9 @@ impl PaystackClient { } /// This method returns a Vec of transactions carried out on your integrations. + /// /// The method takes an Optional parameter for the number of transactions to return. + /// /// If None is passed as the parameter, the last 10 transactions are returned pub async fn list_transactions( &self, @@ -114,4 +116,57 @@ impl PaystackClient { let contents = response.json::().await?; Ok(contents) } + + /// 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); + + let response = self + .client + .get(url) + .bearer_auth(&self.api_key) + .header("Content-Type", "application/json") + .send() + .await?; + + if response.error_for_status_ref().is_err() { + return Err( + RequestNotSuccessful::new(response.status(), response.text().await?).into(), + ); + } + + let content = response.json::().await?; + + Ok(content) + } + + /// 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: Charge) -> PaystackResult { + let url = format!("{}/transaction/charge_authorization", BASE_URL); + + let response = self + .client + .post(url) + .bearer_auth(&self.api_key) + .header("Content-Type", "application/json") + .json(&charge) + .send() + .await?; + + if response.error_for_status_ref().is_err() { + return Err( + RequestNotSuccessful::new(response.status(), response.text().await?).into(), + ); + } + let content = response.json::().await?; + + Ok(content) + } } diff --git a/src/error.rs b/src/error.rs index bb8bf2a..05814bb 100644 --- a/src/error.rs +++ b/src/error.rs @@ -14,6 +14,9 @@ pub enum PaystackError { #[error("Transaction Creation Error: {0}")] TransactionCreation(String), + #[error("Charge Creation Error: {0}")] + ChargeCreation(String), + #[error("Request failed: `{0}`")] RequestNotSuccessful(#[from] RequestNotSuccessful), diff --git a/src/lib.rs b/src/lib.rs index bdc209c..2af1c63 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,7 +61,7 @@ mod response; // public re-exports pub use client::PaystackClient; pub use error::{PaystackError, RequestNotSuccessful}; -pub use request::{Transaction, TransactionBuilder}; +pub use request::{Charge, ChargeBuilder, Transaction, TransactionBuilder}; pub use response::{ Customer, TransactionResponse, TransactionResponseData, TransactionStatus, TransactionStatusData, TransactionStatusList, diff --git a/src/request.rs b/src/request.rs index 4a01e9e..df1aaa4 100644 --- a/src/request.rs +++ b/src/request.rs @@ -12,8 +12,8 @@ use crate::{error, PaystackResult}; /// The struct has the following fields: /// - amount: Amount should be in the smallest unit of the currency e.g. kobo if in NGN and cents if in USD /// - email: Customer's email address -/// - currency (Optional): The transaction currency (NGN, GHS, ZAR or USD). Defaults to your integration currency. -#[derive(serde::Serialize)] +/// - currency (Optional): Currency in which amount should be charged (NGN, GHS, ZAR or USD). Defaults to your integration currency. +#[derive(serde::Serialize, Debug)] pub struct Transaction { amount: String, email: String, @@ -69,3 +69,119 @@ impl TransactionBuilder { }) } } + +/// This struct is used to create a charge body for creating a Charge Authorization using the Paystack API. +/// +/// IMPORTANT: This class can only be created using the ChargeBuilder. +/// +/// The struct has the following fields: +/// - amount: Amount should be in the smallest unit of the currency e.g. kobo if in NGN and cents if in USD +/// - email: Customer's email address +/// - currency (Optional): Currency in which amount should be charged (NGN, GHS, ZAR or USD). Defaults to your integration currency. +/// - authorizatuin_code: A valid authorization code to charge +/// - reference (Optional): Unique transaction reference. Only -, ., = and alphanumeric characters allowed. +/// - channel (Optional): Send us 'card' or 'bank' or 'card','bank' as an array to specify what options to show the user paying +/// - transaction_charge (Optional): A flat fee to charge the subaccount for this transaction (in kobo if currency is NGN, pesewas, +/// if currency is GHS, and cents, if currency is ZAR). This overrides the split percentage set when the subaccount was created. +/// Ideally, you will need to use this if you are splitting in flat rates +/// (since subaccount creation only allows for percentage split). e.g. 7000 for a 70 naira + +#[derive(serde::Serialize, Debug)] +pub struct Charge { + email: String, + amount: String, + authorization_code: String, + reference: Option, + currency: Option, + channel: Option>, + transaction_charge: Option, +} + +/// Builder for the Charge object +#[derive(Default, Clone)] +pub struct ChargeBuilder { + email: Option, + amount: Option, + authorization_code: Option, + reference: Option, + currency: Option, + channel: Option>, + transaction_charge: Option, +} + +impl ChargeBuilder { + /// Create a new instance of the Transaction builder with default properties + pub fn new() -> Self { + ChargeBuilder::default() + } + + /// Specify the transaction email + pub fn email(mut self, email: impl Into) -> Self { + self.email = Some(email.into()); + self + } + + /// Specify the Transaction amount + pub fn amount(mut self, amount: impl Into) -> Self { + self.amount = Some(amount.into()); + self + } + + /// Specify the charge Authorization Code + pub fn authorization_code(mut self, code: impl Into) -> Self { + self.authorization_code = Some(code.into()); + self + } + + /// Specify charge reference + pub fn reference(mut self, reference: impl Into) -> Self { + self.reference = Some(reference.into()); + self + } + + /// Specify charge currency + pub fn currency(mut self, currency: impl Into) -> Self { + self.currency = Some(currency.into()); + self + } + + /// Specify charge channel + pub fn channel(mut self, channel: Vec) -> Self { + self.channel = Some(channel); + self + } + + /// Specify charge transaction charge + pub fn transaction_charge(mut self, transaction_charge: u32) -> Self { + self.transaction_charge = Some(transaction_charge); + self + } + + pub fn build(self) -> PaystackResult { + let Some(email) = self.email else { + return Err(error::PaystackError::ChargeCreation("email is required for creating a charge".to_string())) + }; + + let Some(amount) = self.amount else { + return Err(error::PaystackError::ChargeCreation( + "amount is required for creating charge".to_string() + )) + }; + + let Some(authorization_code) = self.authorization_code else { + return Err(error::PaystackError::ChargeCreation( + "authorization code is required for creating a charge".to_string() + )) + }; + + Ok(Charge { + email, + amount, + authorization_code, + reference: self.reference, + currency: self.currency, + channel: self.channel, + transaction_charge: self.transaction_charge, + }) + } +} diff --git a/src/response.rs b/src/response.rs index 66563ee..5798221 100644 --- a/src/response.rs +++ b/src/response.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; /// This struct represents the response of the Paystack transaction initalization. -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] pub struct TransactionResponse { pub status: bool, pub message: String, @@ -16,7 +16,7 @@ pub struct TransactionResponse { } /// This struct represents the data of the transaction response -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] pub struct TransactionResponseData { pub authorization_url: String, pub access_code: String, @@ -24,7 +24,7 @@ pub struct TransactionResponseData { } /// This struct represents the transaction status response -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] pub struct TransactionStatus { pub status: bool, pub message: String, @@ -32,7 +32,7 @@ pub struct TransactionStatus { } /// This struct represents a list of transaction status -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] pub struct TransactionStatusList { pub status: bool, pub message: String, @@ -40,12 +40,12 @@ pub struct TransactionStatusList { } /// This struct represents the data of the transaction status response -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] pub struct TransactionStatusData { - pub id: Option, + pub id: Option, pub status: Option, pub reference: Option, - pub amount: Option, + pub amount: Option, pub message: Option, pub gateway_response: Option, pub paid_at: Option, @@ -56,10 +56,11 @@ pub struct TransactionStatusData { pub metadata: Option, pub fees: Option, pub customer: Option, + pub authorization: Option, } /// This struct represents the authorization data of the transaction status response -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Clone)] pub struct Authorization { pub authorization_code: Option, pub bin: Option, @@ -77,9 +78,9 @@ pub struct Authorization { } /// This struct represents the Paystack customer data -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct Customer { - pub id: Option, + pub id: Option, pub first_name: Option, pub last_name: Option, pub email: Option, diff --git a/tests/api/transaction.rs b/tests/api/transaction.rs index 179257a..1d7fa74 100644 --- a/tests/api/transaction.rs +++ b/tests/api/transaction.rs @@ -1,6 +1,6 @@ use fake::faker::internet::en::SafeEmail; use fake::Fake; -use paystack::TransactionBuilder; +use paystack::{ChargeBuilder, TransactionBuilder}; use rand::Rng; use crate::helpers::get_paystack_client; @@ -106,3 +106,91 @@ async fn list_specified_number_of_transactions_in_the_integration() { assert!(response.status); assert_eq!("Transactions retrieved", response.message); } + +#[tokio::test] +async fn fetch_transaction_succeeds() { + // Arrange + let client = get_paystack_client(); + let mut rng = rand::thread_rng(); + + // Act + let email: String = SafeEmail().fake(); + let body = TransactionBuilder::new() + .amount(rng.gen_range(100..=100000).to_string()) + .email(email) + .currency("NGN") + .build() + .unwrap(); + + let transaction = client + .initialize_transaction(body) + .await + .expect("unable to initiate transaction"); + + let verified_transaction = client + .verify_transaction(transaction.data.reference.clone()) + .await + .expect("unable to verify transaction"); + + let fetched_transaction = client + .fetch_transactions(verified_transaction.data.id.unwrap()) + .await + .expect("unable to fetch transaction"); + + // Assert + assert_eq!(verified_transaction.data.id, fetched_transaction.data.id); + assert_eq!( + transaction.data.reference.clone(), + fetched_transaction.data.reference.unwrap() + ); +} + +#[tokio::test] +async fn charge_authorization_succeeds() { + // Arrange + let client = get_paystack_client(); + let mut rng = rand::thread_rng(); + + // Act + // In this test, an already created customer in the integration is used + let charge = ChargeBuilder::new() + .amount(rng.gen_range(100..=100000).to_string()) + .email("melyssa@example.net") + .authorization_code("AUTH_9v3686msvt") + .currency("NGN") + .channel(vec!["card".to_string()]) + .transaction_charge(100) + .build() + .unwrap(); + + let charge_response = client + .charge_authorization(charge) + .await + .expect("unable to authorize charge"); + + // Assert + assert!(charge_response.status); + assert_eq!( + charge_response.data.customer.unwrap().email.unwrap(), + "melyssa@example.net" + ); + assert_eq!( + charge_response + .data + .authorization + .clone() + .unwrap() + .channel + .unwrap(), + "card" + ); + assert_eq!( + charge_response + .data + .authorization + .unwrap() + .authorization_code + .unwrap(), + "AUTH_9v3686msvt" + ); +}