From a155523b8a2a2f25f654b29a8de67a9c9eabb8d6 Mon Sep 17 00:00:00 2001 From: intelliDean Date: Wed, 28 Jan 2026 12:09:23 +0100 Subject: [PATCH] restructure the logging --- crates/api/src/handlers/add_bank.rs | 4 +- crates/api/src/handlers/current_user.rs | 4 +- crates/api/src/handlers/delete_bank.rs | 6 +-- crates/api/src/handlers/get_transaction.rs | 2 +- crates/api/src/handlers/health.rs | 2 +- .../api/src/handlers/internal_conversion.rs | 4 +- crates/api/src/handlers/login.rs | 2 +- crates/api/src/handlers/refresh_token.rs | 4 +- crates/api/src/handlers/register.rs | 4 +- crates/api/src/handlers/resolve_account.rs | 4 ++ crates/api/src/handlers/top_up.rs | 4 +- crates/api/src/handlers/transfer_external.rs | 4 +- crates/api/src/handlers/transfer_internal.rs | 31 +++++-------- crates/api/src/handlers/user_transaction.rs | 3 ++ crates/api/src/handlers/withdraw.rs | 7 ++- .../core/src/services/auth_service/login.rs | 7 ++- .../core/src/services/auth_service/logout.rs | 2 +- .../src/services/auth_service/register.rs | 8 +++- .../core/src/services/auth_service/token.rs | 9 +++- .../core/src/services/bank_account_service.rs | 26 +++++++++-- crates/core/src/services/paystack_service.rs | 30 ++++++++++++- .../core/src/services/transaction_service.rs | 30 ++++++++++++- crates/core/src/services/transfer_service.rs | 43 +++++++++++++++++++ 23 files changed, 187 insertions(+), 53 deletions(-) diff --git a/crates/api/src/handlers/add_bank.rs b/crates/api/src/handlers/add_bank.rs index 2099da7..af31c17 100755 --- a/crates/api/src/handlers/add_bank.rs +++ b/crates/api/src/handlers/add_bank.rs @@ -7,7 +7,7 @@ use payego_core::services::bank_account_service::{ }; use payego_primitives::error::ApiErrorResponse; use std::sync::Arc; -use tracing::error; +use tracing::warn; use validator::Validate; #[utoipa::path( @@ -34,7 +34,7 @@ pub async fn add_bank_account( Json(req): Json, ) -> Result<(StatusCode, Json), ApiError> { req.validate().map_err(|e| { - error!("Validation error: {}", e); + warn!("add_bank: validation error"); ApiError::Validation(e) })?; diff --git a/crates/api/src/handlers/current_user.rs b/crates/api/src/handlers/current_user.rs index b07324c..bc80e39 100755 --- a/crates/api/src/handlers/current_user.rs +++ b/crates/api/src/handlers/current_user.rs @@ -11,12 +11,12 @@ use std::sync::Arc; #[utoipa::path( get, path = "/api/user/current", + tag = "User", summary = "Get current authenticated user details", description = "Retrieves profile information for the currently authenticated user based on the JWT bearer token. \ - Returns user data including ID, email, name, etc. \ + Returns user data including ID, email, name, phone, and account status. \ Requires a valid authentication token.", operation_id = "getCurrentUser", - tags = ["Authentication"], responses( (status = 200,description = "Successfully retrieved current user data",body = CurrentUserResponse,), (status = 401,description = "Unauthorized – missing, invalid, or expired token",body = ApiErrorResponse,), diff --git a/crates/api/src/handlers/delete_bank.rs b/crates/api/src/handlers/delete_bank.rs index a698d78..e267396 100644 --- a/crates/api/src/handlers/delete_bank.rs +++ b/crates/api/src/handlers/delete_bank.rs @@ -18,10 +18,10 @@ use uuid::Uuid; ("bank_account_id" = Uuid, Path, description = "Bank account ID to delete") ), responses( - (status = 204, description = "Bank account deleted successfully"), + (status = 200, description = "Bank account deleted successfully", body = DeleteResponse), (status = 401, description = "Unauthorized – missing or invalid token", body = ApiErrorResponse), - (status = 404, description = "Bank account not found", body = ApiErrorResponse), - (status = 409, description = "Bank account cannot be deleted", body = ApiErrorResponse), + (status = 404, description = "Bank account not found or does not belong to user", body = ApiErrorResponse), + (status = 409, description = "Conflict – bank account cannot be deleted (e.g., pending transactions)", body = ApiErrorResponse), (status = 500, description = "Internal server error", body = ApiErrorResponse), ), security(("bearerAuth" = [])), diff --git a/crates/api/src/handlers/get_transaction.rs b/crates/api/src/handlers/get_transaction.rs index d212893..2ee854f 100755 --- a/crates/api/src/handlers/get_transaction.rs +++ b/crates/api/src/handlers/get_transaction.rs @@ -8,6 +8,7 @@ use std::sync::Arc; #[utoipa::path( get, path = "/api/user/transactions", + tag = "Transactions", summary = "Get list of user transactions", description = "Retrieves a paginated list of the authenticated user's transaction history. \ Includes deposits, withdrawals, internal transfers, external transfers, \ @@ -15,7 +16,6 @@ use std::sync::Arc; Results are ordered by creation date (newest first). \ Supports filtering and pagination via query parameters.", operation_id = "getUserTransactions", - tags = ["Transactions"], responses( (status = 200,description = "Successfully retrieved paginated list of transactions",body = TransactionsResponse), diff --git a/crates/api/src/handlers/health.rs b/crates/api/src/handlers/health.rs index 353d376..54055df 100644 --- a/crates/api/src/handlers/health.rs +++ b/crates/api/src/handlers/health.rs @@ -3,7 +3,7 @@ use diesel::prelude::*; use payego_primitives::models::app_state::AppState; use payego_primitives::models::dtos::auth_dto::HealthStatus; use std::sync::Arc; -use tracing::log::error; +use tracing::error; #[utoipa::path( get, diff --git a/crates/api/src/handlers/internal_conversion.rs b/crates/api/src/handlers/internal_conversion.rs index 45a2781..f43a5cc 100755 --- a/crates/api/src/handlers/internal_conversion.rs +++ b/crates/api/src/handlers/internal_conversion.rs @@ -4,7 +4,7 @@ use payego_core::services::conversion_service::{ }; use payego_primitives::error::ApiErrorResponse; use std::sync::Arc; -use tracing::error; +use tracing::warn; use validator::Validate; #[utoipa::path( @@ -35,7 +35,7 @@ pub async fn convert_currency( Json(req): Json, ) -> Result, ApiError> { req.validate().map_err(|e| { - error!("Validation error: {}", e); + warn!("convert_currency: validation error"); ApiError::Validation(e) })?; diff --git a/crates/api/src/handlers/login.rs b/crates/api/src/handlers/login.rs index e20ad41..4e1bb0c 100755 --- a/crates/api/src/handlers/login.rs +++ b/crates/api/src/handlers/login.rs @@ -10,7 +10,7 @@ use std::sync::Arc; path = "/api/auth/login", tag = "Authentication", summary = "Authenticate user and obtain JWT token", - description = "Authenticates a user using email and password\ + description = "Authenticates a user using email and password. \ On success, returns a JWT access token, email and a refresh token that can be used \ for subsequent authenticated requests via the `Authorization: Bearer ` header. \ This is a public endpoint — no prior authentication is required.", diff --git a/crates/api/src/handlers/refresh_token.rs b/crates/api/src/handlers/refresh_token.rs index 79b0934..797c38f 100644 --- a/crates/api/src/handlers/refresh_token.rs +++ b/crates/api/src/handlers/refresh_token.rs @@ -4,7 +4,7 @@ use payego_core::services::auth_service::token::{ }; use payego_primitives::error::ApiErrorResponse; use std::sync::Arc; -use tracing::log::error; +use tracing::warn; use validator::Validate; #[utoipa::path( @@ -38,7 +38,7 @@ pub async fn refresh_token( Json(payload): Json, ) -> Result, ApiError> { payload.validate().map_err(|e| { - error!("Validation error: {}", e); + warn!("refresh_token: validation error"); ApiError::Validation(e) })?; diff --git a/crates/api/src/handlers/register.rs b/crates/api/src/handlers/register.rs index 49d64ae..6b94c22 100755 --- a/crates/api/src/handlers/register.rs +++ b/crates/api/src/handlers/register.rs @@ -7,7 +7,7 @@ use payego_core::services::auth_service::register::{ }; use payego_primitives::error::ApiErrorResponse; use std::sync::Arc; -use tracing::log::error; +use tracing::warn; use validator::Validate; #[utoipa::path( @@ -43,7 +43,7 @@ pub async fn register( let payload = payload.normalize(); payload.validate().map_err(|e| { - error!("Validation error: {}", e); + warn!("register: validation error"); ApiError::Validation(e) })?; diff --git a/crates/api/src/handlers/resolve_account.rs b/crates/api/src/handlers/resolve_account.rs index a9c944a..df1266c 100755 --- a/crates/api/src/handlers/resolve_account.rs +++ b/crates/api/src/handlers/resolve_account.rs @@ -19,6 +19,10 @@ use tracing::info; Requires valid Nigerian bank code (from Paystack supported banks list) and 10-digit account number. \ The endpoint is rate-limited and depends on Paystack availability — cache results when possible for repeated lookups.", operation_id = "resolveAccountName", + params( + ("account_number" = String, Query, description = "10-digit bank account number to verify"), + ("bank_code" = String, Query, description = "Bank code from Paystack supported banks list (e.g., '058' for GTBank)") + ), responses( ( status = 200, description = "Account successfully resolved — returns account name and other verification details", body = ResolveAccountResponse), ( status = 400, description = "Bad request — invalid bank code, account number format, or missing required parameters", body = ApiErrorResponse), diff --git a/crates/api/src/handlers/top_up.rs b/crates/api/src/handlers/top_up.rs index fb97956..93178a1 100755 --- a/crates/api/src/handlers/top_up.rs +++ b/crates/api/src/handlers/top_up.rs @@ -4,7 +4,7 @@ use payego_core::services::payment_service::{ }; use payego_primitives::error::ApiErrorResponse; use std::sync::Arc; -use tracing::log::error; +use tracing::warn; use validator::Validate; #[utoipa::path( @@ -49,7 +49,7 @@ pub async fn top_up( // })?; req.validate().map_err(|e| { - error!("Validation error: {}", e); + warn!("top_up: validation error"); ApiError::Validation(e) })?; diff --git a/crates/api/src/handlers/transfer_external.rs b/crates/api/src/handlers/transfer_external.rs index 2e398ab..0edd398 100755 --- a/crates/api/src/handlers/transfer_external.rs +++ b/crates/api/src/handlers/transfer_external.rs @@ -4,7 +4,7 @@ use payego_core::services::transfer_service::{ }; use payego_primitives::error::ApiErrorResponse; use std::sync::Arc; -use tracing::log::error; +use tracing::warn; use validator::Validate; #[utoipa::path( @@ -45,7 +45,7 @@ pub async fn transfer_external( Json(req): Json, ) -> Result, ApiError> { req.validate().map_err(|e| { - error!("Validation error: {}", e); + warn!("transfer_external: validation error"); ApiError::Validation(e) })?; diff --git a/crates/api/src/handlers/transfer_internal.rs b/crates/api/src/handlers/transfer_internal.rs index 0ee7b96..5d9471a 100755 --- a/crates/api/src/handlers/transfer_internal.rs +++ b/crates/api/src/handlers/transfer_internal.rs @@ -4,7 +4,7 @@ use payego_core::services::transfer_service::{ }; use payego_primitives::error::ApiErrorResponse; use std::sync::Arc; -use tracing::error; +use tracing::warn; use validator::Validate; #[utoipa::path( @@ -45,14 +45,14 @@ pub async fn transfer_internal( payload: Result, axum::extract::rejection::JsonRejection>, ) -> Result, ApiError> { let Json(req) = payload - .map_err(|rejection| { - error!("JSON rejection: {}", rejection); + .map_err(|_rejection| { + warn!("transfer_internal: invalid JSON payload"); ApiError::Validation(validator::ValidationErrors::new()) }) .map_err(|_| ApiError::Internal("Invalid JSON payload".into()))?; req.validate().map_err(|e| { - error!("Validation error: {}", e); + warn!("transfer_internal: validation error"); ApiError::Validation(e) })?; @@ -63,22 +63,11 @@ pub async fn transfer_internal( return Err(ApiError::Internal("Cannot transfer to yourself".into())); } - let recipient_id = req.recipient; + // let recipient_id = req.recipient; - match TransferService::transfer_internal(&state, sender_id, req).await { - Ok(transaction_id) => { - tracing::info!( - "Internal transfer successful from {} to {}", - sender_id, - recipient_id - ); - Ok(Json( - serde_json::json!({ "id": transaction_id.to_string() }), - )) - } - Err(e) => { - tracing::error!("Transfer failed: {}", e); - Err(e) - } - } + let transaction_id = TransferService::transfer_internal(&state, sender_id, req).await?; + + Ok(Json( + serde_json::json!({ "id": transaction_id.to_string() }), + )) } diff --git a/crates/api/src/handlers/user_transaction.rs b/crates/api/src/handlers/user_transaction.rs index bbeae1b..3a95af4 100755 --- a/crates/api/src/handlers/user_transaction.rs +++ b/crates/api/src/handlers/user_transaction.rs @@ -21,6 +21,9 @@ use uuid::Uuid; Only transactions belonging to the authenticated user are accessible. \ Use this endpoint for transaction receipts, status polling, or detailed history views.", operation_id = "getTransactionById", + params( + ("transaction_id" = Uuid, Path, description = "Unique transaction ID (UUID) to retrieve") + ), responses( ( status = 200, description = "Transaction details retrieved successfully", body = TransactionResponse), ( status = 400, description = "Bad request – invalid transaction ID format (not a valid UUID)", body = ApiErrorResponse), diff --git a/crates/api/src/handlers/withdraw.rs b/crates/api/src/handlers/withdraw.rs index a58e708..2e697bf 100755 --- a/crates/api/src/handlers/withdraw.rs +++ b/crates/api/src/handlers/withdraw.rs @@ -5,7 +5,7 @@ use payego_core::services::withdrawal_service::{ }; use payego_primitives::error::ApiErrorResponse; use std::sync::Arc; -use tracing::log::error; +use tracing::warn; use uuid::Uuid; use validator::Validate; @@ -23,6 +23,9 @@ use validator::Validate; Most withdrawals are asynchronous: status starts as `pending` and updates via webhooks (`transfer.success`, `transfer.failed`, etc.). \ Always rely on final webhook confirmation — do **not** assume success from the 200 response alone.", operation_id = "initiateWalletWithdrawal", + params( + ("bank_account_id" = Uuid, Path, description = "ID of the verified bank account to withdraw funds to (must belong to the authenticated user)") + ), request_body( content = WithdrawRequest, description = "Withdrawal details: amount, currency (must match wallet), optional narration/description, \ @@ -53,7 +56,7 @@ pub async fn withdraw( Json(req): Json, ) -> Result, ApiError> { req.validate().map_err(|e| { - error!("Validation error: {}", e); + warn!("withdraw: validation error"); ApiError::Validation(e) })?; diff --git a/crates/core/src/services/auth_service/login.rs b/crates/core/src/services/auth_service/login.rs index db4415b..60abd7f 100644 --- a/crates/core/src/services/auth_service/login.rs +++ b/crates/core/src/services/auth_service/login.rs @@ -11,7 +11,7 @@ pub use payego_primitives::{ user::User, }, }; -use tracing::{error, warn}; +use tracing::{error, info, warn}; pub struct LoginService; @@ -34,6 +34,11 @@ impl LoginService { let refresh_token = Self::create_refresh_token(&mut conn, user.id)?; + info!( + user_id = %user.id, + "User logged in successfully" + ); + Ok(LoginResponse { token, refresh_token, diff --git a/crates/core/src/services/auth_service/logout.rs b/crates/core/src/services/auth_service/logout.rs index cb85d09..8688688 100644 --- a/crates/core/src/services/auth_service/logout.rs +++ b/crates/core/src/services/auth_service/logout.rs @@ -8,7 +8,7 @@ pub use payego_primitives::{ entities::authentication::NewBlacklistedToken, }, }; -use tracing::log::{error, info}; +use tracing::{error, info}; pub struct LogoutService; diff --git a/crates/core/src/services/auth_service/register.rs b/crates/core/src/services/auth_service/register.rs index fe2473e..e443783 100644 --- a/crates/core/src/services/auth_service/register.rs +++ b/crates/core/src/services/auth_service/register.rs @@ -15,7 +15,7 @@ pub use payego_primitives::{ schema::users, }; use secrecy::{ExposeSecret, SecretString}; -use tracing::error; +use tracing::{error, info}; pub struct RegisterService; @@ -52,6 +52,12 @@ impl RegisterService { ApiError::Internal("Authentication service error".into()) })?; + info!( + user_id = %user.id, + email = %user.email, + "User registered successfully" + ); + Ok(RegisterResponse { token, refresh_token, diff --git a/crates/core/src/services/auth_service/token.rs b/crates/core/src/services/auth_service/token.rs index ceae985..9122403 100644 --- a/crates/core/src/services/auth_service/token.rs +++ b/crates/core/src/services/auth_service/token.rs @@ -14,6 +14,7 @@ pub use payego_primitives::{ }; use rand::{distributions::Alphanumeric, Rng}; use sha2::{Digest, Sha256}; +use tracing::{error, info, warn}; use uuid::Uuid; pub struct TokenService; @@ -47,7 +48,7 @@ impl TokenService { raw_token: &str, ) -> Result { let mut conn = state.db.get().map_err(|e| { - tracing::error!("DB connection error: {}", e); + error!("token.refresh: failed to acquire db connection"); ApiError::DatabaseConnection(e.to_string()) })?; @@ -58,11 +59,17 @@ impl TokenService { if let Some(token_record) = token_record { let new_token = Self::generate_refresh_token(&mut conn, token_record.user_id)?; + info!( + user_id = %token_record.user_id, + "Refresh token rotated successfully" + ); + Ok(RefreshResult { user_id: token_record.user_id, new_refresh_token: new_token, }) } else { + warn!("token.refresh: invalid or expired refresh token"); Err(ApiError::Auth(AuthError::InvalidToken( "Invalid or expired refresh token".into(), ))) diff --git a/crates/core/src/services/bank_account_service.rs b/crates/core/src/services/bank_account_service.rs index a75b801..0a400db 100644 --- a/crates/core/src/services/bank_account_service.rs +++ b/crates/core/src/services/bank_account_service.rs @@ -95,6 +95,11 @@ impl BankAccountService { &req.bank_code, &req.account_number, )? { + info!( + user_id = %user_id_val, + bank_code = %req.bank_code, + "Bank account already exists (idempotency check)" + ); return Ok(existing); } @@ -132,7 +137,16 @@ impl BankAccountService { is_verified: true, }; - BankAccountRepository::create(&mut conn, new_account) + let account = BankAccountRepository::create(&mut conn, new_account)?; + + info!( + user_id = %user_id_val, + account_id = %account.id, + bank_code = %req.bank_code, + "Bank account created successfully" + ); + + Ok(account) } pub async fn resolve_account_details( @@ -224,7 +238,7 @@ impl BankAccountService { .map_err(|_| ApiError::Internal("Account number regex misconfigured".into()))? .is_match(account_number) { - error!("Account number must be 10 digits"); + warn!("bank_account.validate: invalid account number format"); return Err(ApiError::BadRequest( "Account number must be 10 digits".to_string(), )); @@ -235,7 +249,7 @@ impl BankAccountService { .map_err(|_| ApiError::Internal("Account number regex misconfigured".into()))? .is_match(bank_code) { - error!("Bank code must be 3–10 digits"); + warn!("bank_account.validate: invalid bank code format"); return Err(ApiError::BadRequest( "Bank code must be 3–10 digits".to_string(), )); @@ -256,6 +270,12 @@ impl BankAccountService { BankAccountRepository::delete_by_id_and_user(&mut conn, bank_account_id, user_id)?; + info!( + user_id = %user_id, + account_id = %bank_account_id, + "Bank account deleted successfully" + ); + Ok(DeleteResponse { account_id: bank_account_id, message: "Bank account deleted successfully".into(), diff --git a/crates/core/src/services/paystack_service.rs b/crates/core/src/services/paystack_service.rs index bf5dd9f..6036e47 100644 --- a/crates/core/src/services/paystack_service.rs +++ b/crates/core/src/services/paystack_service.rs @@ -17,7 +17,7 @@ pub use payego_primitives::{ }; use secrecy::ExposeSecret; use std::sync::Arc; -use tracing::info; +use tracing::{info, warn}; use uuid::Uuid; pub struct PaystackService; @@ -66,18 +66,36 @@ impl PaystackService { // 🔒 Idempotency guard if !matches!(tx.txn_state, PaymentState::Pending) { - info!("Ignoring duplicate webhook for {}", reference); + info!( + transaction_id = %tx.id, + reference = %reference, + current_state = ?tx.txn_state, + "Ignoring duplicate Paystack webhook (idempotency check)" + ); return Ok(()); } match event { "transfer.success" => { TransactionRepository::update_state(conn, tx.id, PaymentState::Completed)?; + + info!( + transaction_id = %tx.id, + reference = %reference, + "Paystack transfer completed successfully" + ); } "transfer.failed" => { TransactionRepository::update_state(conn, tx.id, PaymentState::Failed)?; + warn!( + transaction_id = %tx.id, + reference = %reference, + intent = ?tx.intent, + "Paystack transfer failed" + ); + // 💰 Refund ONLY for payout intents if matches!(tx.intent, TransactionIntent::Payout) { let amount_to_refund = tx.amount.abs(); @@ -107,6 +125,14 @@ impl PaystackService { amount: amount_to_refund, }, )?; + + info!( + transaction_id = %tx.id, + user_id = %tx.user_id, + amount = amount_to_refund, + currency = %tx.currency, + "Refund processed for failed payout" + ); } } diff --git a/crates/core/src/services/transaction_service.rs b/crates/core/src/services/transaction_service.rs index f065c03..b19299b 100644 --- a/crates/core/src/services/transaction_service.rs +++ b/crates/core/src/services/transaction_service.rs @@ -51,12 +51,22 @@ impl TransactionService { // 🔒 Idempotency if tx.txn_state == PaymentState::Completed { - info!("Transaction already completed: {}", tx.reference); + info!( + transaction_id = %tx.id, + reference = %tx.reference, + "Transaction already completed (idempotency check)" + ); return Ok(()); } // 🧪 Currency check if tx.currency.to_string() != currency { + warn!( + transaction_id = %tx.id, + expected_currency = %tx.currency, + received_currency = %currency, + "Payment intent currency mismatch" + ); return Err(ApiError::Payment("Currency mismatch".into())); } @@ -82,6 +92,14 @@ impl TransactionService { }, )?; + info!( + transaction_id = %tx.id, + user_id = %tx.user_id, + amount = amount, + currency = %currency, + "Payment intent succeeded - transaction completed" + ); + Ok(()) }) } @@ -106,6 +124,11 @@ impl TransactionService { tx_ref, PaymentState::Failed, )?; + + warn!( + transaction_reference = %tx_ref, + "Payment intent failed" + ); } Ok(()) @@ -132,6 +155,11 @@ impl TransactionService { tx_ref, PaymentState::Cancelled, )?; + + info!( + transaction_reference = %tx_ref, + "Payment intent cancelled" + ); } Ok(()) diff --git a/crates/core/src/services/transfer_service.rs b/crates/core/src/services/transfer_service.rs index 695711e..0f17bb4 100644 --- a/crates/core/src/services/transfer_service.rs +++ b/crates/core/src/services/transfer_service.rs @@ -22,6 +22,7 @@ use reqwest::{Client, Url}; use secrecy::ExposeSecret; use serde_json::json; use std::sync::Arc; +use tracing::{info, warn}; use uuid::Uuid; pub struct TransferService; @@ -51,6 +52,11 @@ impl TransferService { &req.idempotency_key, )? { if existing.txn_state == PaymentState::Completed { + info!( + transaction_id = %existing.id, + idempotency_key = %req.idempotency_key, + "Internal transfer already completed (idempotency check)" + ); return Ok(existing.id); } } @@ -65,6 +71,13 @@ impl TransferService { WalletRepository::create_if_not_exists(conn, req.recipient, req.currency)?; if sender_wallet.balance < amount_cents { + warn!( + user_id = %sender_id, + available_balance = sender_wallet.balance, + requested_amount = amount_cents, + currency = %req.currency, + "Insufficient balance for internal transfer" + ); return Err(ApiError::Payment("Insufficient balance".into())); } @@ -135,6 +148,15 @@ impl TransferService { WalletRepository::debit(conn, sender_wallet.id, amount_cents)?; WalletRepository::credit(conn, recipient_wallet.id, amount_cents)?; + info!( + transaction_id = %sender_tx.id, + sender_id = %sender_id, + recipient_id = %req.recipient, + amount = amount_cents, + currency = %req.currency, + "Internal transfer completed successfully" + ); + Ok(sender_tx.id) }) } @@ -164,6 +186,11 @@ impl TransferService { if let Some(existing) = TransactionRepository::find_by_idempotency_key(conn, user_id, &req.idempotency_key)? { + info!( + transaction_id = %existing.id, + idempotency_key = %req.idempotency_key, + "External transfer already initiated (idempotency check)" + ); return Ok(existing.id); } @@ -172,6 +199,13 @@ impl TransferService { WalletRepository::find_by_user_and_currency_with_lock(conn, user_id, currency)?; if wallet.balance < amount_minor { + warn!( + user_id = %user_id, + available_balance = wallet.balance, + requested_amount = amount_minor, + currency = %currency, + "Insufficient balance for external transfer" + ); return Err(ApiError::Payment("Insufficient balance".into())); } @@ -229,6 +263,15 @@ impl TransferService { Some(provider_data.transfer_code.to_string()), )?; + info!( + transaction_id = %tx_id, + user_id = %user_id, + amount = amount_minor, + currency = %currency, + provider_status = ?provider_data.status, + "External transfer initiated successfully" + ); + Ok(TransferResponse { transaction_id: tx_id, })