Skip to content

Commit 4ec076c

Browse files
committed
implement transanction history
1 parent 4649337 commit 4ec076c

3 files changed

Lines changed: 200 additions & 2 deletions

File tree

src/docs.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};
8181
wallets::get_banks,
8282
wallets::add_bank_account,
8383
wallets::withdraw_funds,
84+
wallets::list_transactions,
8485
),
8586
8687
components(
@@ -104,6 +105,8 @@ use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme};
104105
wallets::DepositRequest,
105106
wallets::AddBankAccountRequest,
106107
wallets::WithdrawalRequest,
108+
wallets::TransactionListResponse,
109+
wallets::TransactionFilters,
107110
crate::models::Wallet,
108111
crate::models::BankAccount,
109112
crate::models::WalletTransaction,

src/handlers/wallets.rs

Lines changed: 196 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
use axum::{Extension, Json, extract::{Path, State}};
1+
use axum::{Extension, Json, extract::{Path, Query, State}};
22
use bigdecimal::{BigDecimal, ToPrimitive};
3+
use chrono::{DateTime, NaiveDate, Utc};
34
use diesel::prelude::*;
45
use serde::{Deserialize, Serialize};
5-
use utoipa::ToSchema;
6+
use utoipa::{IntoParams, ToSchema};
67
use uuid::Uuid;
78

89
use crate::{
@@ -57,6 +58,50 @@ pub struct WithdrawalRequest {
5758
pub reason: Option<String>,
5859
}
5960

61+
#[derive(Deserialize, IntoParams, ToSchema)]
62+
pub struct TransactionFilters {
63+
/// Optional text search against reference, description, or provider
64+
pub search: Option<String>,
65+
66+
/// Filter by transaction status (pending, successful, failed, reversed)
67+
pub status: Option<WalletTransactionStatusEnum>,
68+
69+
/// Filter by transaction type (deposit, withdrawal, transfer, refund, reward)
70+
pub transaction_type: Option<WalletTransactionTypeEnum>,
71+
72+
/// Filter by start date (inclusive, YYYY-MM-DD)
73+
pub date_from: Option<NaiveDate>,
74+
75+
/// Filter by end date (inclusive, YYYY-MM-DD)
76+
pub date_to: Option<NaiveDate>,
77+
78+
/// Minimum amount filter, e.g. "500.00"
79+
pub amount_min: Option<String>,
80+
81+
/// Maximum amount filter, e.g. "10000.00"
82+
pub amount_max: Option<String>,
83+
84+
/// Page number (starts from 1)
85+
#[serde(default = "default_page")]
86+
pub page: i64,
87+
88+
/// Items per page (default: 20, max: 100)
89+
#[serde(default = "default_page_size")]
90+
pub page_size: i64,
91+
}
92+
93+
fn default_page() -> i64 { 1 }
94+
fn default_page_size() -> i64 { 20 }
95+
96+
#[derive(Serialize, ToSchema)]
97+
pub struct TransactionListResponse {
98+
pub data: Vec<WalletTransaction>,
99+
pub page: i64,
100+
pub page_size: i64,
101+
pub total: i64,
102+
pub total_pages: i64,
103+
}
104+
60105
/// Get wallet balance and transaction history
61106
#[utoipa::path(
62107
get,
@@ -368,6 +413,155 @@ pub async fn withdraw_funds(
368413
Ok(ApiResponse::success_with_message("Withdrawal initiated successfully", transaction))
369414
}
370415

416+
/// List wallet transaction history
417+
///
418+
/// Returns a paginated list of the authenticated user's wallet transactions.
419+
/// Supports filtering by status, type, date range, amount range, and text search.
420+
#[utoipa::path(
421+
get,
422+
path = "/api/wallets/transactions",
423+
params(TransactionFilters),
424+
responses(
425+
(status = 200, body = ApiResponse<TransactionListResponse>),
426+
(status = 401),
427+
(status = 500)
428+
),
429+
tag = "wallets",
430+
security(("bearer_auth" = []))
431+
)]
432+
pub async fn list_transactions(
433+
State(state): State<AppState>,
434+
Extension(current_user): Extension<User>,
435+
Query(filters): Query<TransactionFilters>,
436+
) -> Result<ApiResponse<TransactionListResponse>, AppError> {
437+
let mut conn = state.pool.get()?;
438+
439+
let wallet = get_or_create_wallet(&mut conn, current_user.id)?;
440+
441+
let page = filters.page.max(1);
442+
let page_size = filters.page_size.min(100).max(1);
443+
let offset = (page - 1) * page_size;
444+
445+
// ---- Build base query ----
446+
let mut query = wallet_transactions::table
447+
.filter(wallet_transactions::wallet_id.eq(wallet.id))
448+
.into_boxed();
449+
450+
// Status filter
451+
if let Some(status) = filters.status {
452+
query = query.filter(wallet_transactions::status.eq(status));
453+
}
454+
455+
// Type filter
456+
if let Some(tx_type) = filters.transaction_type {
457+
query = query.filter(wallet_transactions::transaction_type.eq(tx_type));
458+
}
459+
460+
// Date range filters — convert NaiveDate to start/end of day DateTime<Utc>
461+
if let Some(date_from) = filters.date_from {
462+
let start = date_from
463+
.and_hms_opt(0, 0, 0)
464+
.map(|ndt| DateTime::<Utc>::from_naive_utc_and_offset(ndt, Utc))
465+
.unwrap();
466+
query = query.filter(wallet_transactions::created_at.ge(start));
467+
}
468+
469+
if let Some(date_to) = filters.date_to {
470+
let end = date_to
471+
.and_hms_opt(23, 59, 59)
472+
.map(|ndt| DateTime::<Utc>::from_naive_utc_and_offset(ndt, Utc))
473+
.unwrap();
474+
query = query.filter(wallet_transactions::created_at.le(end));
475+
}
476+
477+
// Amount range filters — parse from string
478+
let amount_min = filters.amount_min
479+
.as_deref()
480+
.map(|s| s.parse::<BigDecimal>().ok())
481+
.flatten();
482+
483+
let amount_max = filters.amount_max
484+
.as_deref()
485+
.map(|s| s.parse::<BigDecimal>().ok())
486+
.flatten();
487+
488+
if let Some(ref min) = amount_min {
489+
query = query.filter(wallet_transactions::amount.ge(min.clone()));
490+
}
491+
492+
if let Some(ref max) = amount_max {
493+
query = query.filter(wallet_transactions::amount.le(max.clone()));
494+
}
495+
496+
// Text search — reference, description, or provider
497+
if let Some(ref search) = filters.search {
498+
let pattern = format!("%{}%", search.to_lowercase());
499+
query = query.filter(
500+
wallet_transactions::reference.ilike(pattern.clone())
501+
.or(wallet_transactions::description.ilike(pattern.clone()))
502+
.or(wallet_transactions::provider.ilike(pattern))
503+
);
504+
}
505+
506+
// ---- Count total (clone the predicate) ----
507+
let mut count_query = wallet_transactions::table
508+
.filter(wallet_transactions::wallet_id.eq(wallet.id))
509+
.into_boxed();
510+
511+
if let Some(status) = filters.status {
512+
count_query = count_query.filter(wallet_transactions::status.eq(status));
513+
}
514+
if let Some(tx_type) = filters.transaction_type {
515+
count_query = count_query.filter(wallet_transactions::transaction_type.eq(tx_type));
516+
}
517+
if let Some(date_from) = filters.date_from {
518+
let start = date_from.and_hms_opt(0, 0, 0)
519+
.map(|ndt| DateTime::<Utc>::from_naive_utc_and_offset(ndt, Utc))
520+
.unwrap();
521+
count_query = count_query.filter(wallet_transactions::created_at.ge(start));
522+
}
523+
if let Some(date_to) = filters.date_to {
524+
let end = date_to.and_hms_opt(23, 59, 59)
525+
.map(|ndt| DateTime::<Utc>::from_naive_utc_and_offset(ndt, Utc))
526+
.unwrap();
527+
count_query = count_query.filter(wallet_transactions::created_at.le(end));
528+
}
529+
if let Some(ref min) = amount_min {
530+
count_query = count_query.filter(wallet_transactions::amount.ge(min.clone()));
531+
}
532+
if let Some(ref max) = amount_max {
533+
count_query = count_query.filter(wallet_transactions::amount.le(max.clone()));
534+
}
535+
if let Some(ref search) = filters.search {
536+
let pattern = format!("%{}%", search.to_lowercase());
537+
count_query = count_query.filter(
538+
wallet_transactions::reference.ilike(pattern.clone())
539+
.or(wallet_transactions::description.ilike(pattern.clone()))
540+
.or(wallet_transactions::provider.ilike(pattern))
541+
);
542+
}
543+
544+
let total: i64 = count_query
545+
.count()
546+
.get_result(&mut conn)?;
547+
548+
let transactions = query
549+
.order(wallet_transactions::created_at.desc())
550+
.limit(page_size)
551+
.offset(offset)
552+
.load::<WalletTransaction>(&mut conn)?;
553+
554+
let total_pages = (total + page_size - 1) / page_size;
555+
556+
Ok(ApiResponse::success(TransactionListResponse {
557+
data: transactions,
558+
page,
559+
page_size,
560+
total,
561+
total_pages,
562+
}))
563+
}
564+
371565
// Utility function to get or create a wallet for a user
372566
fn get_or_create_wallet(conn: &mut PgConnection, user_id_val: Uuid) -> Result<Wallet, AppError> {
373567
let wallet = wallets::table

src/routes/wallets.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ pub fn router() -> Router<AppState> {
1313
.route("/banks", get(wallet_handlers::get_banks))
1414
.route("/bank-accounts", post(wallet_handlers::add_bank_account))
1515
.route("/withdraw", post(wallet_handlers::withdraw_funds))
16+
.route("/transactions", get(wallet_handlers::list_transactions))
1617
}

0 commit comments

Comments
 (0)