//! Payment-related dashboard tab handlers. use axum::extract::{Query, State}; use axum::response::IntoResponse; use crate::{ auth::AuthUser, constants::DASHBOARD_TRANSACTION_LIMIT, db, error::Result, helpers, templates::*, types::*, AppState, }; /// Render the HTMX partial for the dashboard payments tab. #[tracing::instrument(skip_all, name = "dashboard_tabs::dashboard_tab_payments")] pub(in crate::routes::pages::dashboard) async fn dashboard_tab_payments( State(state): State, AuthUser(session_user): AuthUser, ) -> Result { let db_user = db::users::get_user_by_id(&state.db, session_user.id) .await? .ok_or(crate::error::AppError::NotFound)?; let user = User::from(&db_user); // Fetch Stripe balance for payout summary let payout_summary = match (&state.stripe, &user.stripe_account_id) { (Some(stripe), Some(account_id)) => { match stripe.get_balance(account_id).await { Ok(balance) => { Some(PayoutSummary { available: helpers::format_revenue(balance.available_cents), pending: helpers::format_revenue(balance.pending_cents), }) } Err(e) => { tracing::warn!(error = ?e, "failed to fetch Stripe balance for payout summary"); None } } } _ => None, }; let incoming_txs = db::transactions::get_transactions_by_seller( &state.db, session_user.id, Some(DASHBOARD_TRANSACTION_LIMIT), ) .await?; let outgoing_txs = db::transactions::get_transactions_by_buyer( &state.db, session_user.id, Some(DASHBOARD_TRANSACTION_LIMIT), ) .await?; let transactions = super::super::super::collect_transactions(incoming_txs, outgoing_txs); // Tips let db_tips = db::tips::get_tips_received(&state.db, session_user.id, 20, 0).await?; let tips_total_cents = db::tips::total_tips_received(&state.db, session_user.id).await?; let tips_count = db::tips::count_tips_received(&state.db, session_user.id).await?; let tips_received: Vec = db_tips .iter() .map(|t| TipReceived { date: t.created_at.format("%Y-%m-%d").to_string(), tipper_name: t.tipper_display_name.clone() .unwrap_or_else(|| t.tipper_username.clone()), amount: helpers::format_price(t.amount_cents), message: t.message.clone(), }) .collect(); // Revenue splits let splits_incoming_cents = db::project_members::total_split_revenue(&state.db, session_user.id).await?; let splits_incoming_count = db::project_members::count_splits_for_recipient(&state.db, session_user.id).await?; let splits_outgoing_cents = db::project_members::total_split_obligations(&state.db, session_user.id).await?; Ok(UserPaymentsTabTemplate { user, payout_summary, transactions, tips_received, tips_total: helpers::format_revenue(tips_total_cents), tips_count, splits_incoming_total: helpers::format_revenue(splits_incoming_cents), splits_incoming_count, splits_outgoing_total: helpers::format_revenue(splits_outgoing_cents), can_create_projects: session_user.can_create_projects, }) } /// Render the HTMX partial for the buyer contacts section (lazy-loaded in Payments tab). #[tracing::instrument(skip_all, name = "dashboard_tabs::dashboard_tab_contacts")] pub(in crate::routes::pages::dashboard) async fn dashboard_tab_contacts( State(state): State, AuthUser(session_user): AuthUser, ) -> Result { let contacts = db::transactions::get_seller_contacts(&state.db, session_user.id).await?; let contact_views: Vec = contacts .into_iter() .map(|c| BuyerContact { username: c.username, email: c.email, total_purchases: c.total_purchases, total_spent: format!("${}.{:02}", c.total_spent_cents / 100, c.total_spent_cents.unsigned_abs() % 100), last_purchase: c.last_purchase_at.format("%b %-d, %Y").to_string(), }) .collect(); Ok(BuyerContactsPartialTemplate { contacts: contact_views }) } /// Render the HTMX partial for the filtered transactions table. #[tracing::instrument(skip_all, name = "dashboard_tabs::dashboard_transactions")] pub(in crate::routes::pages::dashboard) async fn dashboard_transactions( State(state): State, AuthUser(session_user): AuthUser, Query(query): Query, ) -> Result { // Only fetch the direction the user asked for let transactions = match query.r#type.as_deref() { Some("incoming") => { let txs = db::transactions::get_transactions_by_seller( &state.db, session_user.id, Some(DASHBOARD_TRANSACTION_LIMIT), ) .await?; txs.iter().map(Transaction::from_sale).collect() } Some("outgoing") => { let txs = db::transactions::get_transactions_by_buyer( &state.db, session_user.id, Some(DASHBOARD_TRANSACTION_LIMIT), ) .await?; txs.iter().map(Transaction::from_purchase).collect() } _ => { let incoming = db::transactions::get_transactions_by_seller( &state.db, session_user.id, Some(DASHBOARD_TRANSACTION_LIMIT), ) .await?; let outgoing = db::transactions::get_transactions_by_buyer( &state.db, session_user.id, Some(DASHBOARD_TRANSACTION_LIMIT), ) .await?; super::super::super::collect_transactions(incoming, outgoing) } }; Ok(TransactionsTableTemplate { transactions }) }