Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Run test on main
name: Build
on:
push:
branches: ["main", "master"]
branches: ["main", "master", "dev"]
env:
CARGO_TERM_COLOR: always
PAYSTACK_API_KEY: ${{secrets.PAYSTACK_API_KEY}}
Expand Down
22 changes: 15 additions & 7 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,6 @@ on:
jobs:
release:
name: Release Crate
if: |
github.event_name == 'workflow_dispatch' ||
(github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') ||
(github.event_name == 'pull_request' && github.event.pull_request.merged == true)
runs-on: ubuntu-latest
permissions:
contents: write
Expand All @@ -34,12 +30,24 @@ jobs:
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"

- name: Install cargo-release
run: cargo install cargo-release
- name: Install dependencies
run: |
cargo install cargo-release
cargo install git-cliff

- name: Generate Changelog
run: |
git cliff -o CHANGELOG.md -n

- name: Commit Changelog
run: |
git add CHANGELOG.md
git commit -m "chore(changelog): update CHANGELOG.md for release" || echo "No changes to commit"
git push origin HEAD

- name: Perform Release
run: |
LEVEL="${{ github.event.inputs.level || 'patch' }}"
LEVEL="${{ github.event.inputs.level}}"
cargo release $LEVEL --execute --no-confirm
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN}}
13 changes: 12 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

All notable changes to this project will be documented in this file.

## [1.1.1] - 2025-06-26
## [unreleased] - ReleaseDate

### 🚀 Features

Expand All @@ -27,6 +27,17 @@ All notable changes to this project will be documented in this file.
- *(ci)* Remove support for change log generation from main branch to dev branch
- *(ci)* Improved ci/cd to allow release run with a dev is merged into main
- *(ci)* Improved ci/cd to allow release run with a dev is merged into main
- *(ci)* Added username and email to ci/cd pipeline
- *(ci)* Made release pipeline manual
- *(doc)* Bumped up version number
- *(ci)* Added support for checklog development into dev merge
- *(ci)* Added PAT to allow push to protected branch
- *(ci)* Removed automatic running on pull request
- Release paystack-rs version 1.1.1
- *(doc)* Fixed the test passing badge
- *(doc)* Fixed the test passing badge
- *(ci)* Added support for test run when push made to dev branch
- *(ci)* Added support for running changelog generation in CI/CD

## [1.0.0] - 2025-06-24

Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# paystack-rs

[![Rust](https://github.com/morukele/paystack-rs/actions/workflows/rust.yml/badge.svg?branch=main)](https://github.com/morukele/paystack-rs/actions/workflows/rust.yml)
[![Rust](https://github.com/morukele/paystack-rs/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/morukele/paystack-rs/actions/workflows/main.yml)
[![paystack-rs on crates.io](https://img.shields.io/crates/v/paystack-rs.svg)](https://crates.io/crates/paystack-rs)
[![paystack-rs on docs.rs](https://docs.rs/paystack-rs/badge.svg)](https://docs.rs/paystack-rs)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
Expand All @@ -16,8 +16,8 @@ The client currently covers the following section of the API, and the sections t
- [x] Transaction
- [x] Transaction Split
- [x] Terminal
- [ ] Virtual Terminal
- [ ] Customers
- [x] Virtual Terminal
- [x] Customers
- [ ] Dedicated Virtual Account
- [ ] Apple Pay
- [ ] Subaccounts
Expand Down
3 changes: 2 additions & 1 deletion cliff.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ conventional_commits = true
filter_unconventional = true
# Require all commits to be conventional.
# Takes precedence over filter_unconventional.
tag_pattern = "v[0-9]*"
require_conventional = false
# Split commits on newlines, treating each line as an individual commit.
split_commits = false
Expand Down Expand Up @@ -87,7 +88,7 @@ commit_parsers = [
{ message = ".*", group = "<!-- 10 -->💼 Other" },
]
# Exclude commits that are not matched by any commit parser.
filter_commits = false
filter_commits = true
# An array of link parsers for extracting external references, and turning them into URLs, using regex.
link_parsers = []
# Include only the tags that belong to the current branch.
Expand Down
11 changes: 11 additions & 0 deletions release.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,14 @@ pre-release-replacements = [
{ file = "CHANGELOG.md", search = "unreleased", replace = "{{version}}" },
{ file = "CHANGELOG.md", search = "ReleaseDate", replace = "{{date}}" },
]

# Configure the release process
[release]
sign-commit = false
sign-tag = false
push = true
publish = true
allow-branch = ["main"]
consolidate-commits = false
changelog = true
changelog_command = "git cliff -o CHANGELOG.md -n"
9 changes: 6 additions & 3 deletions src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//! =========
//! This file contains the Paystack API client, and it associated endpoints.
use crate::{
HttpClient, SubaccountEndpoints, TerminalEndpoints, TransactionEndpoints,
CustomersEndpoints, HttpClient, SubaccountEndpoints, TerminalEndpoints, TransactionEndpoints,
TransactionSplitEndpoints, VirtualTerminalEndpoints,
};
use std::sync::Arc;
Expand All @@ -11,7 +11,7 @@ use std::sync::Arc;
/// it allows for authentication of the client
pub struct PaystackClient<T: HttpClient + Default> {
/// Transaction API route
pub transaction: TransactionEndpoints<T>,
pub transactions: TransactionEndpoints<T>,
/// Transaction Split API route
pub transaction_split: TransactionSplitEndpoints<T>,
/// Subaccount API route
Expand All @@ -20,18 +20,21 @@ pub struct PaystackClient<T: HttpClient + Default> {
pub terminal: TerminalEndpoints<T>,
/// Virutal Terminal API route
pub virutal_terminal: VirtualTerminalEndpoints<T>,
/// Customers API route
pub customers: CustomersEndpoints<T>,
}

impl<T: HttpClient + Default> PaystackClient<T> {
pub fn new(api_key: String) -> PaystackClient<T> {
let http = Arc::new(T::default());
let key = Arc::new(api_key);
PaystackClient {
transaction: TransactionEndpoints::new(Arc::clone(&key), Arc::clone(&http)),
transactions: TransactionEndpoints::new(Arc::clone(&key), Arc::clone(&http)),
transaction_split: TransactionSplitEndpoints::new(Arc::clone(&key), Arc::clone(&http)),
subaccount: SubaccountEndpoints::new(Arc::clone(&key), Arc::clone(&http)),
terminal: TerminalEndpoints::new(Arc::clone(&key), Arc::clone(&http)),
virutal_terminal: VirtualTerminalEndpoints::new(Arc::clone(&key), Arc::clone(&http)),
customers: CustomersEndpoints::new(Arc::clone(&key), Arc::clone(&http)),
}
}
}
231 changes: 231 additions & 0 deletions src/endpoints/customers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
//! Customers
//! =========
//! Thse Customers API allows you to create and maange customers on your integration

use std::{marker::PhantomData, sync::Arc};

use serde_json::json;

use crate::{
CreateCustomerRequest, CustomerResponseData, HttpClient, PaystackAPIError, PaystackResult,
Response, RiskAction, UpdateCustomerRequest, ValidateCustomerRequest,
};

/// A struct to hold all the functions of the customers API endpoint
#[derive(Debug, Clone)]
pub struct CustomersEndpoints<T: HttpClient + Default> {
/// Paystack API key
key: String,
/// Base URL for the customer route
base_url: String,
/// Http client for the route
http: Arc<T>,
}

impl<T: HttpClient + Default> CustomersEndpoints<T> {
/// Constructor
pub fn new(key: Arc<String>, http: Arc<T>) -> CustomersEndpoints<T> {
let base_url = String::from("https://api.paystack.co/customer");
CustomersEndpoints {
key: key.to_string(),
base_url,
http,
}
}

/// Create customer on your integration
///
/// It takes the following parameters:
/// - create_customer_request: contains the information about the customer to be created.
/// It should be built with `CreateCustomerRequestBuilder`.
pub async fn create_customer(
&self,
create_customer_request: CreateCustomerRequest,
) -> PaystackResult<CustomerResponseData> {
let url = format!("{}", self.base_url);
let body = serde_json::to_value(create_customer_request)
.map_err(|e| PaystackAPIError::Customer(e.to_string()))?;

let response = self.http.post(&url, &self.key, &body).await;

match response {
Ok(response) => {
let parsed_response: Response<CustomerResponseData> =
serde_json::from_str(&response)
.map_err(|e| PaystackAPIError::Customer(e.to_string()))?;

Ok(parsed_response)
}
Err(e) => Err(PaystackAPIError::Customer(e.to_string())),
}
}

/// List customers available on your integration
///
/// It takes the following parameters:
/// - `per_page`: Specify how many records you want to retreive per page. If not specified, default value of 50.
/// - `page`: Specify exactly waht page you want to retreive. If not specified, default value of 1.
pub async fn list_customers(
&self,
per_page: Option<u8>,
page: Option<u8>,
) -> PaystackResult<Vec<CustomerResponseData>> {
let url = format!("{}", self.base_url);

let per_page = per_page.unwrap_or(50).to_string();
let page = page.unwrap_or(1).to_string();
let query = vec![("perPage", per_page.as_str()), ("page", page.as_str())];

let response = self.http.get(&url, &self.key, Some(&query)).await;

match response {
Ok(response) => {
let parsed_response: Response<Vec<CustomerResponseData>> =
serde_json::from_str(&response)
.map_err(|e| PaystackAPIError::Customer(e.to_string()))?;

Ok(parsed_response)
}
Err(e) => Err(PaystackAPIError::Customer(e.to_string())),
}
}

/// Get details of a customer on your integration.
///
/// It takes the following parameters:
/// - `email_or_code`: An `email`or `customer code` for the customer you want to fetch.
pub async fn fetch_customer(
&self,
email_or_code: String,
) -> PaystackResult<CustomerResponseData> {
let url = format!("{}/{}", self.base_url, email_or_code);

let response = self.http.get(&url, &self.key, None).await;

match response {
Ok(response) => {
let parsed_response: Response<CustomerResponseData> =
serde_json::from_str(&response)
.map_err(|e| PaystackAPIError::Customer(e.to_string()))?;

Ok(parsed_response)
}
Err(e) => Err(PaystackAPIError::Customer(e.to_string())),
}
}

/// Update a customer's details on your integration
///
/// It takes the following parameters:
/// - `customer_code`: The customer's code
/// - `update_customer_request`: The data to update the customer with.
/// It should be created with the `UpdateCustomerRequestBuilder` struct.
pub async fn update_customer(
&self,
customer_code: String,
update_customer_request: UpdateCustomerRequest,
) -> PaystackResult<CustomerResponseData> {
let url = format!("{}/{}", self.base_url, customer_code);
let body = serde_json::to_value(update_customer_request)
.map_err(|e| PaystackAPIError::Customer(e.to_string()))?;

let response = self.http.put(&url, &self.key, &body).await;

match response {
Ok(response) => {
let parsed_response: Response<CustomerResponseData> =
serde_json::from_str(&response)
.map_err(|e| PaystackAPIError::Customer(e.to_string()))?;

Ok(parsed_response)
}
Err(e) => Err(PaystackAPIError::Customer(e.to_string())),
}
}

/// Validate a customer's identity
///
/// It takes in the following parameters:
/// - `customer_code`: email, or customer code of customer to be identified.
/// - `customer_validation_request`: The data to validate the customer with.
/// It should be created with the `ValidateCustomerRequestBuilder` struct.
pub async fn validate_customer(
&self,
customer_code: String,
customer_validation_request: ValidateCustomerRequest,
) -> PaystackResult<PhantomData<String>> {
let url = format!("{}/{}/identification", self.base_url, customer_code);
let body = serde_json::to_value(customer_validation_request)
.map_err(|e| PaystackAPIError::Customer(e.to_string()))?;

let response = self.http.post(&url, &self.key, &body).await;

match response {
Ok(response) => {
let parsed_response: Response<PhantomData<String>> =
serde_json::from_str(&response)
.map_err(|e| PaystackAPIError::Customer(e.to_string()))?;

Ok(parsed_response)
}
Err(e) => Err(PaystackAPIError::Customer(e.to_string())),
}
}

/// Whitelist or blacklist a customer on your integration
///
/// It takes in the following parameters:
/// - `customer_code`: Customer's code, or email address.
/// - `risk_action`: One of the possible risk actions for the customer.
pub async fn whitelist_or_blacklist_customer(
&self,
customer_code: String,
risk_action: RiskAction,
) -> PaystackResult<CustomerResponseData> {
let url = format!("{}/set_risk_action", self.base_url);
let body = json!({
"customer": customer_code,
"risk_action": risk_action
});

let response = self.http.post(&url, &self.key, &body).await;

match response {
Ok(response) => {
let parsed_response: Response<CustomerResponseData> =
serde_json::from_str(&response)
.map_err(|e| PaystackAPIError::Customer(e.to_string()))?;

Ok(parsed_response)
}
Err(e) => Err(PaystackAPIError::Customer(e.to_string())),
}
}

/// Deactivate an authorization when the card needs to be forgotten
///
/// It takes the following parameters:
/// - `authorization_code`: Authorization code to be deactivated.
pub async fn deactivate_authorization(
&self,
authorization_code: String,
) -> PaystackResult<PhantomData<String>> {
let url = format!("{}/authorization/deactivate", self.base_url);
let body = json!({
"authorization_code": authorization_code
});

let response = self.http.post(&url, &self.key, &body).await;

match response {
Ok(response) => {
let parsed_response: Response<PhantomData<String>> =
serde_json::from_str(&response)
.map_err(|e| PaystackAPIError::Customer(e.to_string()))?;

Ok(parsed_response)
}
Err(e) => Err(PaystackAPIError::Customer(e.to_string())),
}
}
}
Loading
Loading