From b247905c01088373d4c30bd265c9adb1451ee658 Mon Sep 17 00:00:00 2001 From: Oghenemarho Orukele Date: Tue, 24 Jun 2025 13:48:14 +0200 Subject: [PATCH 1/2] refactored library for new developmenet. Ensure all test pass. Added an identifier struct to streamline transaction fetching --- src/endpoints/subaccount.rs | 3 ++ src/endpoints/terminal.rs | 3 ++ src/endpoints/transaction.rs | 36 +++++++++++----------- src/endpoints/transaction_split.rs | 3 ++ src/models/authorization.rs | 32 ++++++++++++++++++++ src/models/charge.rs | 24 ++++++++++++++- src/models/customer.rs | 6 ++-- src/models/mod.rs | 2 ++ src/models/response.rs | 31 ------------------- src/models/transaction.rs | 28 ++++++++++------- tests/api/charge.rs | 24 ++++----------- tests/api/transaction.rs | 48 ++++++++---------------------- tests/api/transaction_split.rs | 2 +- 13 files changed, 124 insertions(+), 118 deletions(-) create mode 100644 src/models/authorization.rs diff --git a/src/endpoints/subaccount.rs b/src/endpoints/subaccount.rs index 6f58046..8d443e9 100644 --- a/src/endpoints/subaccount.rs +++ b/src/endpoints/subaccount.rs @@ -12,8 +12,11 @@ use std::sync::Arc; /// A struct to hold all functions in the subaccount API route #[derive(Debug, Clone)] pub struct SubaccountEndpoints { + /// Paystack API Key key: String, + /// Base URL for the transaction route base_url: String, + /// Http client for the route http: Arc, } diff --git a/src/endpoints/terminal.rs b/src/endpoints/terminal.rs index 72cbe17..f9b3a28 100644 --- a/src/endpoints/terminal.rs +++ b/src/endpoints/terminal.rs @@ -9,8 +9,11 @@ use crate::{EventRequest, HttpClient, PaystackResult, SendEventResponseData}; /// A struct to hold all the functions of the terminal API endpoint #[derive(Debug, Clone)] pub struct TerminalEndpoints { + /// Paystack API Key key: String, + /// Base URL for the transaction route base_url: String, + /// Http client for the route http: Arc, } diff --git a/src/endpoints/transaction.rs b/src/endpoints/transaction.rs index beb15d5..dd7938c 100644 --- a/src/endpoints/transaction.rs +++ b/src/endpoints/transaction.rs @@ -3,9 +3,10 @@ //! The Transaction route allows to create and manage payments on your integration. use crate::{ - ChargeRequest, Currency, ExportTransactionData, HttpClient, PartialDebitTransactionRequest, - PaystackAPIError, PaystackResult, Response, Status, TransactionRequest, - TransactionResponseData, TransactionStatusData, TransactionTimelineData, TransactionTotalData, + ChargeRequest, ChargeResponseData, Currency, ExportTransactionData, HttpClient, + PartialDebitTransactionRequest, PaystackAPIError, PaystackResult, Response, Status, + TransactionIdentifier, TransactionRequest, TransactionResponseData, TransactionStatusData, + TransactionTimelineData, TransactionTotalData, }; use std::sync::Arc; @@ -70,6 +71,8 @@ impl TransactionEndpoints { let response = self.http.get(&url, &self.key, None).await; + dbg!("{:#?}", &response); + match response { Ok(response) => { let parsed_response: Response = @@ -101,6 +104,8 @@ impl TransactionEndpoints { let response = self.http.get(&url, &self.key, Some(&query)).await; + dbg!("{:#?}", &response); + match response { Ok(response) => { let parsed_response: Response> = @@ -118,7 +123,7 @@ impl TransactionEndpoints { /// This method take the ID of the desired transaction as a parameter pub async fn fetch_transactions( &self, - transaction_id: u32, + transaction_id: u64, ) -> PaystackResult { let url = format!("{}/{}", self.base_url, transaction_id); @@ -142,7 +147,7 @@ impl TransactionEndpoints { pub async fn charge_authorization( &self, charge_request: ChargeRequest, - ) -> PaystackResult { + ) -> PaystackResult { let url = format!("{}/charge_authorization", self.base_url); let body = serde_json::to_value(charge_request) .map_err(|e| PaystackAPIError::Transaction(e.to_string()))?; @@ -151,9 +156,8 @@ impl TransactionEndpoints { match response { Ok(response) => { - let parsed_response: Response = - serde_json::from_str(&response) - .map_err(|e| PaystackAPIError::Transaction(e.to_string()))?; + let parsed_response: Response = serde_json::from_str(&response) + .map_err(|e| PaystackAPIError::Charge(e.to_string()))?; Ok(parsed_response) } @@ -163,20 +167,18 @@ impl TransactionEndpoints { /// View the timeline of a transaction. /// - /// This method takes in the Transaction id or reference as a parameter + /// This method takes in the TransactionIdentifier as a parameter pub async fn view_transaction_timeline( &self, - id: Option, - reference: Option<&str>, + identifier: TransactionIdentifier, ) -> PaystackResult { // This is a hacky implementation to ensure that the transaction reference or id is not empty. // If they are empty, a new url without them as parameter is created. - let url = match (id, reference) { - (Some(id), None) => Ok(format!("{}/timeline/{}", self.base_url, id)), - (None, Some(reference)) => Ok(format!("{}/timeline/{}", self.base_url, &reference)), - _ => Err(PaystackAPIError::Transaction( - "Transaction Id or Reference is need to view transaction timeline".to_string(), - )), + let url = match identifier { + TransactionIdentifier::Id(id) => Ok(format!("{}/timeline/{}", self.base_url, id)), + TransactionIdentifier::Reference(reference) => { + Ok(format!("{}/timeline/{}", self.base_url, &reference)) + } }?; // propagate the error upstream let response = self.http.get(&url, &self.key, None).await; diff --git a/src/endpoints/transaction_split.rs b/src/endpoints/transaction_split.rs index a5f061f..a20a822 100644 --- a/src/endpoints/transaction_split.rs +++ b/src/endpoints/transaction_split.rs @@ -12,8 +12,11 @@ use std::sync::Arc; /// A struct to hold all the functions of the transaction split API endpoint #[derive(Debug, Clone)] pub struct TransactionSplitEndpoints { + /// Paystack API Key key: String, + /// Base URL for the transaction route base_url: String, + /// Http client for the route http: Arc, } diff --git a/src/models/authorization.rs b/src/models/authorization.rs new file mode 100644 index 0000000..9ce5cd6 --- /dev/null +++ b/src/models/authorization.rs @@ -0,0 +1,32 @@ +use serde::{Deserialize, Serialize}; + +/// This struct represents the authorization data of the transaction status response +#[derive(Debug, Deserialize, Serialize, Clone, Default)] +pub struct Authorization { + /// Authorization code generated for the Transaction. + pub authorization_code: Option, + /// Bin number for Transaction authorization. + pub bin: Option, + /// Last 4 digits of authorized card. + pub last4: Option, + /// Authorized card expiry month. + pub exp_month: Option, + /// Authorized card expiry year. + pub exp_year: Option, + /// Authorization channel. It could be `card` or `bank`. + pub channel: Option, + /// Type of card used in the Authorization + pub card_type: Option, + /// Name of bank associated with the Authorization. + pub bank: Option, + /// Country code of the Authorization. + pub country_code: Option, + /// Brand of of the Authorization if it is a card. + pub brand: Option, + /// Specifies if the Authorization is reusable. + pub reusable: Option, + /// Signature of the Authorization. + pub signature: Option, + /// Name of the account associated with the authorization. + pub account_name: Option, +} diff --git a/src/models/charge.rs b/src/models/charge.rs index e10930a..bb6a155 100644 --- a/src/models/charge.rs +++ b/src/models/charge.rs @@ -5,7 +5,9 @@ use crate::{Channel, Currency}; use derive_builder::Builder; -use serde::Serialize; +use serde::{Deserialize, Serialize}; + +use super::{Authorization, Customer}; /// This struct is used to create a charge body for creating a Charge Authorization using the Paystack API. /// The struct is constructed using the `ChargeBodyBuilder` @@ -49,3 +51,23 @@ pub struct ChargeRequest { #[builder(default = "None")] queue: Option, } + +/// This struct represents the charge response +#[derive(Deserialize, Serialize, Debug, Clone, Default)] +pub struct ChargeResponseData { + pub amount: u64, + pub currency: String, + pub transaction_date: String, + pub status: String, + pub reference: String, + pub metadata: Option, + pub gateway_response: String, + pub message: Option, + pub channel: String, + pub ip_address: Option, + pub fees: u64, + pub authorization: Authorization, + pub customer: Customer, + pub plan: Option, + pub id: Option, +} diff --git a/src/models/customer.rs b/src/models/customer.rs index f7ff244..7abd755 100644 --- a/src/models/customer.rs +++ b/src/models/customer.rs @@ -1,16 +1,16 @@ use serde::{Deserialize, Serialize}; /// This struct represents the Paystack customer data -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Deserialize, Serialize, Clone, Default)] pub struct Customer { /// Customer's Id. - pub id: Option, + pub id: u32, /// Customer's first name. pub first_name: Option, /// Customer's last name. pub last_name: Option, /// Customer's email address. - pub email: Option, + pub email: String, /// Customer's code. pub customer_code: String, /// Customer's phone number. diff --git a/src/models/mod.rs b/src/models/mod.rs index 41cf80e..66f9333 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -1,3 +1,4 @@ +pub mod authorization; pub mod bearer; pub mod channel; pub mod charge; @@ -12,6 +13,7 @@ pub mod transaction; pub mod transaction_split; // public re-export +pub use authorization::*; pub use bearer::*; pub use channel::*; pub use charge::*; diff --git a/src/models/response.rs b/src/models/response.rs index 452dcd7..fcf5d29 100644 --- a/src/models/response.rs +++ b/src/models/response.rs @@ -38,34 +38,3 @@ pub struct Meta { #[serde(deserialize_with = "string_or_number_to_u16")] pub page_count: u16, } - -/// This struct represents the authorization data of the transaction status response -#[derive(Debug, Deserialize, Serialize, Clone)] -pub struct Authorization { - /// Authorization code generated for the Transaction. - pub authorization_code: Option, - /// Bin number for Transaction authorization. - pub bin: Option, - /// Last 4 digits of authorized card. - pub last4: Option, - /// Authorized card expiry month. - pub exp_month: Option, - /// Authorized card expiry year. - pub exp_year: Option, - /// Authorization channel. It could be `card` or `bank`. - pub channel: Option, - /// Type of card used in the Authorization - pub card_type: Option, - /// Name of bank associated with the Authorization. - pub bank: Option, - /// Country code of the Authorization. - pub country_code: Option, - /// Brand of of the Authorization if it is a card. - pub brand: Option, - /// Specifies if the Authorization is reusable. - pub reusable: Option, - /// Signature of the Authorization. - pub signature: Option, - /// Name of the account associated with the authorization. - pub account_name: Option, -} diff --git a/src/models/transaction.rs b/src/models/transaction.rs index 152e6b9..e9ee18f 100644 --- a/src/models/transaction.rs +++ b/src/models/transaction.rs @@ -87,25 +87,25 @@ pub struct TransactionResponseData { #[derive(Deserialize, Serialize, Debug, Clone, Default)] pub struct TransactionStatusData { /// Id of the Transaction - pub id: Option, + pub id: u64, /// Status of the Transaction. It can be `success`, `abandoned` or `failed` - pub status: Option, + pub status: String, /// Reference of the Transaction - pub reference: Option, + pub reference: String, /// Amount of the transaction in the lowest denomination of the currency e.g. Kobo for NGN and cent for USD. - pub amount: Option, + pub amount: u32, /// Message from the transaction. pub message: Option, /// Response from the payment gateway. - pub gateway_response: Option, + pub gateway_response: String, /// Time the Transaction was completed. pub paid_at: Option, /// Time the Transaction was created. - pub created_at: Option, + pub created_at: String, /// Transaction channel. It can be `card` or `bank`. - pub channel: Option, + pub channel: String, /// Currency code of the Transaction e.g. `NGN for Nigerian Naira` and `USD for US Dollar`. - pub currency: Option, + pub currency: String, /// IP address of the computers the Transaction has passed through. pub ip_address: Option, /// Meta data associated with the Transaction. @@ -113,9 +113,9 @@ pub struct TransactionStatusData { /// Transaction fees to override the default fees specified in the integration. pub fees: Option, /// Transaction customer data. - pub customer: Option, + pub customer: Customer, /// Transaction authorization data. - pub authorization: Option, + pub authorization: Authorization, } /// This struct represents the transaction timeline data. @@ -186,6 +186,14 @@ pub struct ExportTransactionData { pub path: String, } +/// Transaction identifier. +/// +/// It can either be a transaction reference or a transaction ID +pub enum TransactionIdentifier { + Id(u64), + Reference(String), +} + #[cfg(test)] mod test { use super::*; diff --git a/tests/api/charge.rs b/tests/api/charge.rs index 51bc5df..177deea 100644 --- a/tests/api/charge.rs +++ b/tests/api/charge.rs @@ -28,28 +28,14 @@ async fn charge_authorization_succeeds() -> Result<(), Box> { // Assert assert!(charge_response.status); + assert_eq!(charge_response.data.customer.email, "susanna@example.net"); assert_eq!( - charge_response.data.customer.unwrap().email.unwrap(), - "susanna@example.net" + charge_response.data.authorization.clone().channel, + Some("card".into()) ); assert_eq!( - charge_response - .data - .authorization - .clone() - .unwrap() - .channel - .unwrap(), - "card" - ); - assert_eq!( - charge_response - .data - .authorization - .unwrap() - .authorization_code - .unwrap(), - "AUTH_ik4t69fo2y" + charge_response.data.authorization.authorization_code, + Some("AUTH_ik4t69fo2y".into()) ); Ok(()) diff --git a/tests/api/transaction.rs b/tests/api/transaction.rs index 449c1f6..1007d04 100644 --- a/tests/api/transaction.rs +++ b/tests/api/transaction.rs @@ -2,7 +2,8 @@ use crate::helpers::get_paystack_client; use fake::faker::internet::en::SafeEmail; use fake::Fake; use paystack::{ - Channel, Currency, PartialDebitTransactionRequestBuilder, Status, TransactionRequestBuilder, + Channel, Currency, PartialDebitTransactionRequestBuilder, Status, TransactionIdentifier, + TransactionRequestBuilder, }; use rand::Rng; @@ -108,7 +109,7 @@ async fn valid_transaction_is_verified() { // Assert assert!(response.status); assert_eq!(response.message, "Verification successful"); - assert!(response.data.status.is_some()); + assert_eq!(response.data.status, "abandoned"); } #[tokio::test] @@ -161,7 +162,7 @@ async fn fetch_transaction_succeeds() { let fetched_transaction = client .transaction - .fetch_transactions(response.data[0].id.unwrap()) + .fetch_transactions(response.data[0].id) .await .expect("unable to fetch transaction"); @@ -185,9 +186,11 @@ async fn view_transaction_timeline_passes_with_id() { .await .expect("unable to get list of integrated transactions"); + let identifier = TransactionIdentifier::Id(response.data[0].id); + let transaction_timeline = client .transaction - .view_transaction_timeline(response.data[0].id, None) + .view_transaction_timeline(identifier) .await .expect("unable to get transaction timeline"); @@ -209,10 +212,11 @@ async fn view_transaction_timeline_passes_with_reference() { .expect("unable to get list of integrated transactions"); // println!("{:#?}", response); - let reference = &response.data[0].reference.clone().unwrap(); + let reference = response.data[0].reference.clone(); + let identifier = TransactionIdentifier::Reference(reference); let transaction_timeline = client .transaction - .view_transaction_timeline(None, Some(reference)) + .view_transaction_timeline(identifier) .await .expect("unable to get transaction timeline"); @@ -221,29 +225,6 @@ async fn view_transaction_timeline_passes_with_reference() { assert_eq!(transaction_timeline.message, "Timeline retrieved"); } -#[tokio::test] -async fn view_transaction_timeline_fails_without_id_or_reference() { - // Arrange - let client = get_paystack_client(); - - // Act - let res = client - .transaction - .view_transaction_timeline(None, None) - .await; - - // Assert - match res { - Ok(_) => (), - Err(e) => { - let res = e.to_string(); - assert!( - res.contains("Transaction Id or Reference is need to view transaction timeline") - ); - } - } -} - #[tokio::test] async fn get_transaction_total_is_successful() { // Arrange @@ -294,12 +275,8 @@ async fn partial_debit_transaction_passes_or_fails_depending_on_merchant_status( .expect("Unable to get transaction list"); let transaction = transaction.data[0].clone(); - let email = transaction.customer.unwrap().email.unwrap(); - let authorization_code = transaction - .authorization - .unwrap() - .authorization_code - .unwrap(); + let email = transaction.customer.email; + let authorization_code = transaction.authorization.authorization_code.unwrap(); let body = PartialDebitTransactionRequestBuilder::default() .email(email) .amount("10000".to_string()) @@ -315,7 +292,6 @@ async fn partial_debit_transaction_passes_or_fails_depending_on_merchant_status( Ok(result) => { assert!(result.status); assert_eq!(result.message, "Charge attempted"); - assert!(result.data.customer.unwrap().id.is_some()) } Err(error) => { let error = error.to_string(); diff --git a/tests/api/transaction_split.rs b/tests/api/transaction_split.rs index c4c118d..acb6e92 100644 --- a/tests/api/transaction_split.rs +++ b/tests/api/transaction_split.rs @@ -140,7 +140,7 @@ async fn list_transaction_splits_in_the_integration() { if let Ok(data) = res { assert!(data.status); assert_eq!(data.message, "Split retrieved".to_string()); - assert_eq!(data.data.len(), 1); + assert!(data.data.len() > 0); let transaction_split = data.data.first().unwrap(); assert_eq!( From 154863d3ae8333986a1c088fd53d68062ce1dc1b Mon Sep 17 00:00:00 2001 From: Oghenemarho Orukele Date: Tue, 24 Jun 2025 14:05:54 +0200 Subject: [PATCH 2/2] removed metadata field from Customer struct --- .github/workflows/PR.yml | 21 +-------------------- src/endpoints/transaction.rs | 2 -- src/models/customer.rs | 2 -- 3 files changed, 1 insertion(+), 24 deletions(-) diff --git a/.github/workflows/PR.yml b/.github/workflows/PR.yml index 4eba722..b3a2e0d 100644 --- a/.github/workflows/PR.yml +++ b/.github/workflows/PR.yml @@ -1,7 +1,7 @@ name: Rust test on PR fork on: pull_request_target: - types: [opened, synchronize] + types: [opened, synchronize] env: CARGO_TERM_COLOR: always PAYSTACK_API_KEY: ${{secrets.PAYSTACK_API_KEY}} @@ -35,22 +35,3 @@ jobs: run: cargo build --verbose - name: Run tests run: cargo test --verbose - - coverage: - runs-on: ubuntu-latest - env: - CARGO_TERM_COLOR: always - steps: - - uses: actions/checkout@v4 - - name: Install Rust - run: rustup update stable - - name: Install cargo-llvm-cov - uses: taiki-e/install-action@cargo-llvm-cov - - name: Generate code coverage - run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v3 - with: - files: lcov.info - fail_ci_if_error: true - diff --git a/src/endpoints/transaction.rs b/src/endpoints/transaction.rs index dd7938c..6371e6a 100644 --- a/src/endpoints/transaction.rs +++ b/src/endpoints/transaction.rs @@ -71,8 +71,6 @@ impl TransactionEndpoints { let response = self.http.get(&url, &self.key, None).await; - dbg!("{:#?}", &response); - match response { Ok(response) => { let parsed_response: Response = diff --git a/src/models/customer.rs b/src/models/customer.rs index 7abd755..2490043 100644 --- a/src/models/customer.rs +++ b/src/models/customer.rs @@ -15,8 +15,6 @@ pub struct Customer { pub customer_code: String, /// Customer's phone number. pub phone: Option, - /// Customer's metadata. - pub metadata: Option, /// Customer's risk action. pub risk_action: Option, /// Customer's phone number in international format.