Skip to main content

max / makenotwork

6.0 KB · 162 lines History Blame Raw
1 //! Payment-related dashboard tab handlers.
2
3 use axum::extract::{Query, State};
4 use axum::response::IntoResponse;
5
6 use crate::{
7 auth::AuthUser,
8 constants::DASHBOARD_TRANSACTION_LIMIT,
9 db,
10 error::Result,
11 helpers,
12 templates::*,
13 types::*,
14 AppState,
15 };
16
17 /// Render the HTMX partial for the dashboard payments tab.
18 #[tracing::instrument(skip_all, name = "dashboard_tabs::dashboard_tab_payments")]
19 pub(in crate::routes::pages::dashboard) async fn dashboard_tab_payments(
20 State(state): State<AppState>,
21 AuthUser(session_user): AuthUser,
22 ) -> Result<impl IntoResponse> {
23 let db_user = db::users::get_user_by_id(&state.db, session_user.id)
24 .await?
25 .ok_or(crate::error::AppError::NotFound)?;
26
27 let user = User::from(&db_user);
28
29 // Fetch Stripe balance for payout summary
30 let payout_summary = match (&state.stripe, &user.stripe_account_id) {
31 (Some(stripe), Some(account_id)) => {
32 match stripe.get_balance(account_id).await {
33 Ok(balance) => {
34 Some(PayoutSummary {
35 available: helpers::format_revenue(balance.available_cents),
36 pending: helpers::format_revenue(balance.pending_cents),
37 })
38 }
39 Err(e) => {
40 tracing::warn!(error = ?e, "failed to fetch Stripe balance for payout summary");
41 None
42 }
43 }
44 }
45 _ => None,
46 };
47
48 let incoming_txs = db::transactions::get_transactions_by_seller(
49 &state.db,
50 session_user.id,
51 Some(DASHBOARD_TRANSACTION_LIMIT),
52 )
53 .await?;
54 let outgoing_txs = db::transactions::get_transactions_by_buyer(
55 &state.db,
56 session_user.id,
57 Some(DASHBOARD_TRANSACTION_LIMIT),
58 )
59 .await?;
60 let transactions = super::super::super::collect_transactions(incoming_txs, outgoing_txs);
61
62 // Tips
63 let db_tips = db::tips::get_tips_received(&state.db, session_user.id, 20, 0).await?;
64 let tips_total_cents = db::tips::total_tips_received(&state.db, session_user.id).await?;
65 let tips_count = db::tips::count_tips_received(&state.db, session_user.id).await?;
66 let tips_received: Vec<TipReceived> = db_tips
67 .iter()
68 .map(|t| TipReceived {
69 date: t.created_at.format("%Y-%m-%d").to_string(),
70 tipper_name: t.tipper_display_name.clone()
71 .unwrap_or_else(|| t.tipper_username.clone()),
72 amount: helpers::format_price(t.amount_cents),
73 message: t.message.clone(),
74 })
75 .collect();
76
77 // Revenue splits
78 let splits_incoming_cents = db::project_members::total_split_revenue(&state.db, session_user.id).await?;
79 let splits_incoming_count = db::project_members::count_splits_for_recipient(&state.db, session_user.id).await?;
80 let splits_outgoing_cents = db::project_members::total_split_obligations(&state.db, session_user.id).await?;
81
82 Ok(UserPaymentsTabTemplate {
83 user,
84 payout_summary,
85 transactions,
86 tips_received,
87 tips_total: helpers::format_revenue(tips_total_cents),
88 tips_count,
89 splits_incoming_total: helpers::format_revenue(splits_incoming_cents),
90 splits_incoming_count,
91 splits_outgoing_total: helpers::format_revenue(splits_outgoing_cents),
92 can_create_projects: session_user.can_create_projects,
93 })
94 }
95
96 /// Render the HTMX partial for the buyer contacts section (lazy-loaded in Payments tab).
97 #[tracing::instrument(skip_all, name = "dashboard_tabs::dashboard_tab_contacts")]
98 pub(in crate::routes::pages::dashboard) async fn dashboard_tab_contacts(
99 State(state): State<AppState>,
100 AuthUser(session_user): AuthUser,
101 ) -> Result<impl IntoResponse> {
102 let contacts = db::transactions::get_seller_contacts(&state.db, session_user.id).await?;
103 let contact_views: Vec<BuyerContact> = contacts
104 .into_iter()
105 .map(|c| BuyerContact {
106 username: c.username,
107 email: c.email,
108 total_purchases: c.total_purchases,
109 total_spent: format!("${}.{:02}", c.total_spent_cents / 100, c.total_spent_cents.unsigned_abs() % 100),
110 last_purchase: c.last_purchase_at.format("%b %-d, %Y").to_string(),
111 })
112 .collect();
113 Ok(BuyerContactsPartialTemplate { contacts: contact_views })
114 }
115
116 /// Render the HTMX partial for the filtered transactions table.
117 #[tracing::instrument(skip_all, name = "dashboard_tabs::dashboard_transactions")]
118 pub(in crate::routes::pages::dashboard) async fn dashboard_transactions(
119 State(state): State<AppState>,
120 AuthUser(session_user): AuthUser,
121 Query(query): Query<super::super::super::TransactionQuery>,
122 ) -> Result<impl IntoResponse> {
123 // Only fetch the direction the user asked for
124 let transactions = match query.r#type.as_deref() {
125 Some("incoming") => {
126 let txs = db::transactions::get_transactions_by_seller(
127 &state.db,
128 session_user.id,
129 Some(DASHBOARD_TRANSACTION_LIMIT),
130 )
131 .await?;
132 txs.iter().map(Transaction::from_sale).collect()
133 }
134 Some("outgoing") => {
135 let txs = db::transactions::get_transactions_by_buyer(
136 &state.db,
137 session_user.id,
138 Some(DASHBOARD_TRANSACTION_LIMIT),
139 )
140 .await?;
141 txs.iter().map(Transaction::from_purchase).collect()
142 }
143 _ => {
144 let incoming = db::transactions::get_transactions_by_seller(
145 &state.db,
146 session_user.id,
147 Some(DASHBOARD_TRANSACTION_LIMIT),
148 )
149 .await?;
150 let outgoing = db::transactions::get_transactions_by_buyer(
151 &state.db,
152 session_user.id,
153 Some(DASHBOARD_TRANSACTION_LIMIT),
154 )
155 .await?;
156 super::super::super::collect_transactions(incoming, outgoing)
157 }
158 };
159
160 Ok(TransactionsTableTemplate { transactions })
161 }
162