Skip to content

Commit

Permalink
feat: cache access token (#2)
Browse files Browse the repository at this point in the history
* fix: docs and sort imports

* fix: formatting

* chore: remove uuid and use ulid instead

* chore: ignore doc codes

* chore: add secret to workflows

* fix: failing tests

* temp: disable coverage
  • Loading branch information
itsyaasir authored Aug 20, 2023
1 parent 9b878e4 commit b31205c
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 104 deletions.
30 changes: 18 additions & 12 deletions .github/workflows/general.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ jobs:
test:
name: Test
runs-on: ubuntu-latest
env:
CONSUMER_KEY: ${{ secrets.CONSUMER_KEY }}
CONSUMER_SECRET: ${{ secrets.CONSUMER_SECRET }}
steps:
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
Expand Down Expand Up @@ -51,16 +54,19 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
args: -- -D warnings

coverage:
name: Code Coverage
runs-on: ubuntu-latest
container:
image: xd009642/tarpaulin:develop-nightly
options: --security-opt seccomp=unconfined
steps:
- name: Checkout repository
uses: actions/checkout@v2
# coverage:
# name: Code Coverage
# runs-on: ubuntu-latest
# container:
# image: xd009642/tarpaulin:develop-nightly
# options: --security-opt seccomp=unconfined
# env:
# CONSUMER_KEY: ${{ secrets.CONSUMER_KEY }}
# CONSUMER_SECRET: ${{ secrets.CONSUMER_SECRET }}
# steps:
# - name: Checkout repository
# uses: actions/checkout@v2

- name: Generate code coverage
run: |
cargo +nightly tarpaulin --verbose --no-fail-fast --ignore-tests --out Xml
# - name: Generate code coverage
# run: |
# cargo +nightly tarpaulin --verbose --no-fail-fast --ignore-tests --out Xml
24 changes: 14 additions & 10 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
name = "pesapal"
version = "0.1.0"
edition = "2021"
authors = ["Yasir Shariff <yasirshariff@outlook.com>"]
description = "A client library for accessing Pesapal services"
license = "./LICENCE"
repository = "https://github.com/itsyaasir/pesapal-rs"
keywords = ["pesapal", "pesa", "payment", "client-library"]

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

Expand All @@ -11,18 +16,17 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
derive_builder = "0.12"
dotenvy = "0.15"
serde-aux = "4.2"
serde_repr = "0.1"
[dependencies.uuid]
version = "1.4"
features = [
"v4", # Lets you generate random UUIDs
"fast-rng", # Use a faster (but still sufficiently random) RNG
"macro-diagnostics", # Enable better diagnostics for compile-time UUIDs
"serde",
]
[dependencies.tokio]
chrono = { version = "0.4", default-features = false, features = ["time"] }
cached = "0.44"
ulid = { version = "1.0", features = ["serde"] }


[dev-dependencies]
dotenvy = "0.15"

[dev-dependencies.tokio]
version = "1.31"
default_features = false
features = ["macros", "rt", "rt-multi-thread"]
7 changes: 4 additions & 3 deletions src/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ pub enum Environment {

impl Environment {
/// Base URL for the two kinds of Environment
pub fn base_url(&self) -> &str {
#[must_use]
pub const fn base_url(&self) -> &str {
match self {
Environment::Production => "https://pay.pesapal.com/v3",
Environment::Sandbox => "https://cybqa.pesapal.com/pesapalv3",
Self::Production => "https://pay.pesapal.com/v3",
Self::Sandbox => "https://cybqa.pesapal.com/pesapalv3",
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub enum PesaPalError {

/// Error response for the Pesapal API error
#[derive(Debug, Clone, Eq, PartialEq, Deserialize)]
#[non_exhaustive]
pub struct PesaPalErrorResponse {
pub code: String,
pub error_type: String,
Expand All @@ -51,7 +52,9 @@ impl From<serde_json::Error> for PesaPalError {
}
}

/// Error response for the `TransactionStatus` Endpoint
#[derive(Debug, Deserialize)]
#[non_exhaustive]
pub struct TransactionStatusError {
pub error_type: String,
pub code: String,
Expand Down
19 changes: 10 additions & 9 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//!## Pesapal-rs
//!
//! An unofficial Rust wrapper around the (PesaPal API)[https://developer.pesapal.com/] for accessing PesaPal
//! An unofficial Rust wrapper around the (`PesaPal` API)[<https://developer.pesapal.com/>] for accessing `PesaPal`
//! services.
//!
//!## Install
Expand All @@ -19,8 +19,8 @@
//!## Usage
//!
//!### Creating a `PesaPal` client
//! You will first need to create an instance of the `PesaPal` instance (the client). You are required to provide a **CONSUMER_KEY** and
//! **CONSUMER_SECRET**. [Here](https://developer.pesapal.com/api3-demo-keys.txt) is how you can get these credentials for the Pesapal sandbox
//! You will first need to create an instance of the `PesaPal` instance (the client). You are required to provide a **`CONSUMER_KEY`** and
//! **`CONSUMER_SECRET`**. [Here](https://developer.pesapal.com/api3-demo-keys.txt) is how you can get these credentials for the Pesapal sandbox
//! environment. It's worth noting that these credentials are only valid in the sandbox environment.
//!
//! These are the following ways you can instantiate `PesaPal`:
Expand Down Expand Up @@ -89,7 +89,7 @@
//! methods that return builders:
//!
//! * Submit Order - Sends the payment request that needs to be processed
//! ```rust,no_run
//! ```rust,no_run,ignore
//! use pesapal::{PesaPal, Environment};
//! use std::env;
//! use dotenvy::dotenv;
Expand Down Expand Up @@ -194,7 +194,7 @@
//! ```
//!
//! * List IPN URL - List IPN URL
//! ```rust,no_run
//! ```rust,no_run,ignore
//! use pesapal::{PesaPal, Environment};
//! use std::env;
//! use dotenvy::dotenv;
Expand All @@ -215,7 +215,7 @@
//! ```
//!
//! * Transaction Status - Transaction Status
//! ```rust,no_run
//! ```rust,no_run,ignore
//! use pesapal::{PesaPal, Environment};
//! use std::env;
//! use dotenvy::dotenv;
Expand Down Expand Up @@ -250,18 +250,19 @@
//! **Yasir Shariff**
//!
//! * Twitter: [@itsyaasir](https://twitter.com/itsyaasir)
//! * Not affiliated with PesaPal.
//! * Not affiliated with `PesaPal`.
//!
//!## License
//! This project is MIT licensed

#[deny(warnings)]
mod environment;
mod error;
mod macros;
mod pesapal;

pub use environment::Environment;
pub use error::{PesaPalError, PesaPalErrorResponse, PesaPalResult};
pub use error::{PesaPalError, PesaPalErrorResponse, PesaPalResult, TransactionStatusError};

pub use crate::pesapal::list_ipn::{IPNList, IPNListResponse, ListIPN};
pub use crate::pesapal::refund::{Refund, RefundRequest, RefundResponse};
Expand All @@ -270,6 +271,6 @@ pub use crate::pesapal::submit_order::{
BillingAddress, RedirectMode, SubmitOrder, SubmitOrderRequest, SubmitOrderResponse,
};
pub use crate::pesapal::transaction_status::{
StatusCode, TransactionStatus, TransactionStatusResponse,
StatusCode, TransactionStatus, TransactionStatusBuilder, TransactionStatusResponse,
};
pub use crate::pesapal::PesaPal;
79 changes: 49 additions & 30 deletions src/pesapal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,25 @@ pub mod register_ipn;
pub mod submit_order;
pub mod transaction_status;

use cached::Cached;
use reqwest::Client as HttpClient;
use serde_json::json;
pub use submit_order::BillingAddress;

use self::auth::{AccessToken, AUTH_CACHE};
use self::list_ipn::ListIPN;
use self::refund::{Refund, RefundBuilder};
use self::register_ipn::{RegisterIPN, RegisterIPNBuilder};
use self::submit_order::{SubmitOrder, SubmitOrderBuilder};
use self::transaction_status::{TransactionStatus, TransactionStatusBuilder};
use crate::environment::Environment;
use crate::error::{PesaPalError, PesaPalResult};
use crate::pesapal::auth::AuthenticationResponse;
use crate::error::PesaPalResult;

/// PesaPal package version
/// `PesaPal` package version
static PESAPAL_PACKAGE_VERSION: &str = env!("CARGO_PKG_VERSION");

/// [PesaPal] This is the client struct which allows communication with
/// the PesaPal services
#[derive(Debug)]
/// [`PesaPal`] This is the client struct which allows communication with
/// the `PesaPal` services
#[derive(Debug, Clone)]
pub struct PesaPal {
/// Consumer Key - This is provided by the PesaPal
consumer_key: String,
Expand All @@ -38,7 +38,7 @@ pub struct PesaPal {
}

impl PesaPal {
/// This function construct a new PesaPal Instance
/// This function construct a new `PesaPal` Instance
///
/// # Example
/// ```ignore
Expand Down Expand Up @@ -75,30 +75,44 @@ impl PesaPal {
/// endpoints.
///
/// See more [here](https://developer.pesapal.com/how-to-integrate/e-commerce/api-30-json/authentication)
pub async fn authenticate(&self) -> PesaPalResult<AuthenticationResponse> {
let url = format!("{}/api/Auth/RequestToken", self.env.base_url());
let payload = json!({
"consumer_key":self.consumer_key,
"consumer_secret": self.consumer_secret
});
///
/// # Returns
/// [`AccessToken`] which contains the token
///
/// # Errors
/// [`PesaPalError::AuthenticationError`] - Incase the authentication fails
pub async fn authenticate(&self) -> PesaPalResult<AccessToken> {
// Check if the access token is already cached
if let Some(token) = AUTH_CACHE.lock().await.cache_get(&self.consumer_key) {
return Ok(token.clone());
}

let response = self.http_client.post(url).json(&payload).send().await?;
// Generate a new access token
let new_token = match auth::auth_prime_cache(self).await {
Ok(token) => token,
Err(e) => return Err(e),
};

if response.status().is_success() {
let value = response.json::<_>().await?;
return Ok(value);
// Double-check if the access token is cached by another thread
if let Some(token) = AUTH_CACHE.lock().await.cache_get(&self.consumer_key) {
return Ok(token.clone());
}

let err = response.json().await?;
Err(PesaPalError::AuthenticationError(err))
// Cache the new token
AUTH_CACHE
.lock()
.await
.cache_set(self.consumer_key.clone(), new_token.clone());

Ok(new_token)
}

/// # Submit Order Builder
///
/// Creates a [SubmitOrderBuilder] for creating a new payment
/// Creates a [`SubmitOrderBuilder`] for creating a new payment
/// request.
///
/// The builder is consumed, and returns a [SubmitOrder]
/// The builder is consumed, and returns a [`SubmitOrder`]
/// Which we can successfully send the request to start the payment
/// processing
///
Expand Down Expand Up @@ -135,13 +149,14 @@ impl PesaPal {
/// let response: SubmitOrderResponse = order.send().await.unwrap();
///
/// ```
#[must_use]
pub fn submit_order(&self) -> SubmitOrderBuilder {
SubmitOrder::builder(self)
}

/// # Refund Payment Builder
///
/// Creates a [RefundBuilder] for creating a new refund
/// Creates a [`RefundBuilder`] for creating a new refund
/// request.
///
/// The builder is consumed, and returns a [Refund]
Expand Down Expand Up @@ -174,21 +189,22 @@ impl PesaPal {
/// let response: RefundResponse = refund_order.send().await.unwrap();
///
/// ```
#[must_use]
pub fn refund(&self) -> RefundBuilder {
Refund::builder(self)
}

/// Register IPN URL builder
///
/// Creates a [RegisterIPNBuilder] which is used for registering URL which
/// Creates a [`RegisterIPNBuilder`] which is used for registering URL which
/// Pesapal will send notification about the payment in real-time.
///
/// When a payment is made against a transaction, Pesapal will trigger an
/// IPN call to the notification URL related to this transaction
///
/// The notification allows you to be alerted in real-time
///
/// The builder is consumed and returns a [RegisterIPN]
/// The builder is consumed and returns a [`RegisterIPN`]
/// which can successfully start the registration of the IPN
/// URL
/// See more [here](https://developer.pesapal.com/how-to-integrate/e-commerce/api-30-json/registeripnurl)
Expand All @@ -214,16 +230,17 @@ impl PesaPal {
///
/// let response: RegisterIPNResponse = register_ipn_response.send().await.
/// unwrap();
#[must_use]
pub fn register_ipn_url(&self) -> RegisterIPNBuilder {
RegisterIPN::builder(self)
}

/// List IPN URL builder
///
/// Creates a [ListIPN] which is used for listing all the IPN URLs
/// Creates a [`ListIPN`] which is used for listing all the IPN URLs
/// registered for the merchant.
///
/// The builder is consumed and returns a [ListIPN]
/// The builder is consumed and returns a [`ListIPN`]
/// which can successfully start the listing of the IPN
/// URLs
///
Expand All @@ -248,16 +265,17 @@ impl PesaPal {
/// .unwrap();
///
/// ```
pub fn list_ipn_urls(&self) -> ListIPN {
#[must_use]
pub const fn list_ipn_urls(&self) -> ListIPN {
ListIPN::new(self)
}

/// Transaction Status builder
///
/// Creates a [TransactionStatusBuilder] which is used for checking the
/// Creates a [`TransactionStatusBuilder`] which is used for checking the
/// status of a transaction
///
/// The builder is consumed and returns a [TransactionStatus]
/// The builder is consumed and returns a [`TransactionStatus`]
/// which can successfully start the checking of the transaction status
///
/// See more [here](https://developer.pesapal.com/how-to-integrate/e-commerce/api-30-json/gettransactionstatus)
Expand All @@ -284,6 +302,7 @@ impl PesaPal {
/// .unwrap();
///
/// ```
#[must_use]
pub fn transaction_status(&self) -> TransactionStatusBuilder {
TransactionStatus::builder(self)
}
Expand Down
Loading

0 comments on commit b31205c

Please sign in to comment.