Remove InvoiceLookupError
This commit is contained in:
+19
-11
@@ -7,7 +7,7 @@ use crate::env::Env;
|
||||
use crate::models::{Activity, RELAY_STATUS_ACTIVE, RELAY_STATUS_DELINQUENT};
|
||||
use crate::query::Query;
|
||||
use crate::robot::Robot;
|
||||
use crate::stripe::{InvoiceLookupError, Stripe};
|
||||
use crate::stripe::Stripe;
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
const MANUAL_LIGHTNING_PAYMENT_DM: &str = "Payment is due for your relay subscription. Please visit the application to complete a manual Lightning payment.";
|
||||
@@ -631,29 +631,30 @@ impl Billing {
|
||||
|
||||
// --- Public API helpers ---
|
||||
|
||||
/// Returns `Ok(None)` if Stripe has no such invoice; the route turns that into a 404.
|
||||
pub async fn get_invoice_with_tenant(
|
||||
&self,
|
||||
invoice_id: &str,
|
||||
) -> std::result::Result<(serde_json::Value, crate::models::Tenant), InvoiceLookupError> {
|
||||
let invoice = self.stripe.get_invoice(invoice_id).await?;
|
||||
) -> Result<Option<(serde_json::Value, crate::models::Tenant)>> {
|
||||
let Some(invoice) = self.stripe.get_invoice(invoice_id).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
let customer_id = invoice["customer"]
|
||||
.as_str()
|
||||
.ok_or_else(|| InvoiceLookupError::Internal(anyhow!("invoice missing customer")))?;
|
||||
.ok_or_else(|| anyhow!("invoice missing customer"))?;
|
||||
let tenant = self
|
||||
.query
|
||||
.get_tenant_by_stripe_customer_id(customer_id)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
InvoiceLookupError::Internal(anyhow!("tenant not found for customer"))
|
||||
})?;
|
||||
Ok((invoice, tenant))
|
||||
.ok_or_else(|| anyhow!("tenant not found for customer"))?;
|
||||
Ok(Some((invoice, tenant)))
|
||||
}
|
||||
|
||||
pub async fn reconcile_manual_lightning_invoice(
|
||||
&self,
|
||||
invoice_id: &str,
|
||||
invoice: &serde_json::Value,
|
||||
) -> std::result::Result<serde_json::Value, InvoiceLookupError> {
|
||||
) -> Result<serde_json::Value> {
|
||||
self.reconcile_manual_lightning_invoice_if_settled(invoice_id, invoice)
|
||||
.await
|
||||
}
|
||||
@@ -853,7 +854,7 @@ impl Billing {
|
||||
&self,
|
||||
invoice_id: &str,
|
||||
invoice: &serde_json::Value,
|
||||
) -> std::result::Result<serde_json::Value, InvoiceLookupError> {
|
||||
) -> Result<serde_json::Value> {
|
||||
if invoice["status"].as_str().unwrap_or_default() != "open" {
|
||||
return Ok(invoice.clone());
|
||||
}
|
||||
@@ -890,7 +891,14 @@ impl Billing {
|
||||
);
|
||||
}
|
||||
|
||||
self.stripe.get_invoice(invoice_id).await
|
||||
// The invoice existed when we called pay_invoice_out_of_band a moment ago;
|
||||
// if Stripe suddenly returns 404, fall back to the pre-reconcile snapshot
|
||||
// rather than failing the request.
|
||||
Ok(self
|
||||
.stripe
|
||||
.get_invoice(invoice_id)
|
||||
.await?
|
||||
.unwrap_or_else(|| invoice.clone()))
|
||||
}
|
||||
|
||||
async fn is_manual_lightning_invoice_settled(&self, bolt11: &str) -> Result<bool> {
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::extract::{Path, State};
|
||||
use reqwest::StatusCode;
|
||||
|
||||
use crate::api::{Api, AuthedPubkey};
|
||||
use crate::stripe::InvoiceLookupError;
|
||||
use crate::web::{ApiError, ApiResult, bad_request, internal, not_found, ok};
|
||||
use crate::web::{ApiResult, bad_request, internal, not_found, ok};
|
||||
|
||||
pub async fn list_tenant_invoices(
|
||||
State(api): State<Arc<Api>>,
|
||||
@@ -32,14 +30,15 @@ pub async fn get_invoice(
|
||||
.billing
|
||||
.get_invoice_with_tenant(&id)
|
||||
.await
|
||||
.map_err(map_invoice_lookup_error)?;
|
||||
.map_err(internal)?
|
||||
.ok_or_else(|| not_found("invoice not found"))?;
|
||||
api.require_admin_or_tenant(&auth, &tenant.pubkey)?;
|
||||
|
||||
let invoice = api
|
||||
.billing
|
||||
.reconcile_manual_lightning_invoice(&id, &invoice)
|
||||
.await
|
||||
.map_err(map_invoice_lookup_error)?;
|
||||
.map_err(internal)?;
|
||||
|
||||
ok(invoice)
|
||||
}
|
||||
@@ -53,14 +52,15 @@ pub async fn get_invoice_bolt11(
|
||||
.billing
|
||||
.get_invoice_with_tenant(&id)
|
||||
.await
|
||||
.map_err(map_invoice_lookup_error)?;
|
||||
.map_err(internal)?
|
||||
.ok_or_else(|| not_found("invoice not found"))?;
|
||||
api.require_admin_or_tenant(&auth, &tenant.pubkey)?;
|
||||
|
||||
let invoice = api
|
||||
.billing
|
||||
.reconcile_manual_lightning_invoice(&id, &invoice)
|
||||
.await
|
||||
.map_err(map_invoice_lookup_error)?;
|
||||
.map_err(internal)?;
|
||||
|
||||
let status = invoice["status"].as_str().unwrap_or_default();
|
||||
if status != "open" {
|
||||
@@ -77,16 +77,3 @@ pub async fn get_invoice_bolt11(
|
||||
.map_err(internal)?;
|
||||
ok(serde_json::json!({ "bolt11": bolt11 }))
|
||||
}
|
||||
|
||||
fn map_invoice_lookup_error(error: InvoiceLookupError) -> ApiError {
|
||||
match error {
|
||||
InvoiceLookupError::StripeClient { status } => match status {
|
||||
StatusCode::NOT_FOUND => not_found("invoice not found"),
|
||||
_ => {
|
||||
tracing::warn!(%status, "stripe invoice request returned unexpected status");
|
||||
internal("invoice request rejected")
|
||||
}
|
||||
},
|
||||
InvoiceLookupError::Internal(error) => internal(error),
|
||||
}
|
||||
}
|
||||
|
||||
+6
-47
@@ -14,44 +14,6 @@ type HmacSha256 = Hmac<Sha256>;
|
||||
const STRIPE_API: &str = "https://api.stripe.com/v1";
|
||||
const WEBHOOK_TOLERANCE_SECS: i64 = 300;
|
||||
|
||||
/// Error returned by invoice lookups, distinguishing a Stripe 4xx (e.g. "no such
|
||||
/// invoice") — which callers usually want to surface as a client error — from an
|
||||
/// internal failure.
|
||||
#[derive(Debug)]
|
||||
pub enum InvoiceLookupError {
|
||||
StripeClient { status: reqwest::StatusCode },
|
||||
Internal(anyhow::Error),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for InvoiceLookupError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::StripeClient { status } => {
|
||||
write!(
|
||||
f,
|
||||
"stripe invoice lookup failed with status {}",
|
||||
status.as_u16()
|
||||
)
|
||||
}
|
||||
Self::Internal(error) => write!(f, "{error}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for InvoiceLookupError {}
|
||||
|
||||
impl From<anyhow::Error> for InvoiceLookupError {
|
||||
fn from(value: anyhow::Error) -> Self {
|
||||
Self::Internal(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<reqwest::Error> for InvoiceLookupError {
|
||||
fn from(value: reqwest::Error) -> Self {
|
||||
Self::Internal(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// A Stripe webhook event with its signature already verified.
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct Event {
|
||||
@@ -263,23 +225,20 @@ impl Stripe {
|
||||
Ok(body["data"].clone())
|
||||
}
|
||||
|
||||
pub async fn get_invoice(
|
||||
&self,
|
||||
invoice_id: &str,
|
||||
) -> std::result::Result<serde_json::Value, InvoiceLookupError> {
|
||||
/// Fetches an invoice, returning `None` if Stripe no longer knows about it
|
||||
/// (so callers can surface a 404 to the user).
|
||||
pub async fn get_invoice(&self, invoice_id: &str) -> Result<Option<serde_json::Value>> {
|
||||
let resp = self
|
||||
.http
|
||||
.get(format!("{STRIPE_API}/invoices/{invoice_id}"))
|
||||
.bearer_auth(&self.secret_key)
|
||||
.send()
|
||||
.await?;
|
||||
if resp.status().is_client_error() {
|
||||
return Err(InvoiceLookupError::StripeClient {
|
||||
status: resp.status(),
|
||||
});
|
||||
if resp.status() == reqwest::StatusCode::NOT_FOUND {
|
||||
return Ok(None);
|
||||
}
|
||||
let body: serde_json::Value = error_for_status(resp).await?.json().await?;
|
||||
Ok(body)
|
||||
Ok(Some(body))
|
||||
}
|
||||
|
||||
pub async fn pay_invoice(&self, invoice_id: &str) -> Result<()> {
|
||||
|
||||
Reference in New Issue
Block a user