Skip to main content

max / makenotwork

2.3 KB · 69 lines History Blame Raw
1 //! Self-service refund endpoint for creators.
2
3 use axum::extract::{Path, State};
4 use axum::response::IntoResponse;
5 use axum::Json;
6 use serde::Deserialize;
7
8 use crate::{
9 auth::AuthUser,
10 db::{self, ItemId, TransactionId},
11 error::{AppError, Result},
12 AppState,
13 };
14
15 use super::super::verify_item_ownership;
16
17 #[derive(Debug, Deserialize)]
18 pub struct RefundRequest {
19 pub transaction_id: TransactionId,
20 }
21
22 /// Issue a full refund for a transaction on this item.
23 ///
24 /// The refund is sent to Stripe; the existing `charge.refunded` webhook
25 /// handler marks the transaction as refunded, revokes license keys, and
26 /// decrements the sales count.
27 #[tracing::instrument(skip_all, name = "items::refund_transaction")]
28 pub(in crate::routes::api) async fn refund_transaction(
29 State(state): State<AppState>,
30 AuthUser(user): AuthUser,
31 Path(id): Path<ItemId>,
32 Json(req): Json<RefundRequest>,
33 ) -> Result<impl IntoResponse> {
34 user.check_not_suspended()?;
35 verify_item_ownership(&state, id, user.id).await?;
36
37 // Fetch the transaction and validate it belongs to this item
38 let tx = db::transactions::get_transaction_by_id(&state.db, req.transaction_id)
39 .await?
40 .ok_or(AppError::NotFound)?;
41
42 if tx.item_id != Some(id) {
43 return Err(AppError::Forbidden);
44 }
45 if tx.seller_id != Some(user.id) {
46 return Err(AppError::Forbidden);
47 }
48 if tx.status != db::TransactionStatus::Completed {
49 return Err(AppError::BadRequest("Transaction is not in a refundable state".into()));
50 }
51
52 let payment_intent_id = tx.stripe_payment_intent_id.as_deref()
53 .ok_or_else(|| AppError::BadRequest("No payment intent — free claims cannot be refunded".into()))?;
54
55 // Get the creator's Stripe connected account ID
56 let seller = db::users::get_user_by_id(&state.db, user.id)
57 .await?
58 .ok_or(AppError::NotFound)?;
59 let stripe_account_id = seller.stripe_account_id.as_deref()
60 .ok_or_else(|| AppError::BadRequest("No Stripe account connected".into()))?;
61
62 // Issue the refund via Stripe — the webhook handler does the rest
63 let stripe = state.stripe.as_ref()
64 .ok_or_else(|| AppError::ServiceUnavailable("Stripe is not configured".to_string()))?;
65 stripe.create_refund(payment_intent_id, stripe_account_id).await?;
66
67 Ok(Json(serde_json::json!({ "ok": true })))
68 }
69