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::models::{Activity, RELAY_STATUS_ACTIVE, RELAY_STATUS_DELINQUENT};
|
||||||
use crate::query::Query;
|
use crate::query::Query;
|
||||||
use crate::robot::Robot;
|
use crate::robot::Robot;
|
||||||
use crate::stripe::{InvoiceLookupError, Stripe};
|
use crate::stripe::Stripe;
|
||||||
use crate::wallet::Wallet;
|
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.";
|
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 ---
|
// --- 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(
|
pub async fn get_invoice_with_tenant(
|
||||||
&self,
|
&self,
|
||||||
invoice_id: &str,
|
invoice_id: &str,
|
||||||
) -> std::result::Result<(serde_json::Value, crate::models::Tenant), InvoiceLookupError> {
|
) -> Result<Option<(serde_json::Value, crate::models::Tenant)>> {
|
||||||
let invoice = self.stripe.get_invoice(invoice_id).await?;
|
let Some(invoice) = self.stripe.get_invoice(invoice_id).await? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
let customer_id = invoice["customer"]
|
let customer_id = invoice["customer"]
|
||||||
.as_str()
|
.as_str()
|
||||||
.ok_or_else(|| InvoiceLookupError::Internal(anyhow!("invoice missing customer")))?;
|
.ok_or_else(|| anyhow!("invoice missing customer"))?;
|
||||||
let tenant = self
|
let tenant = self
|
||||||
.query
|
.query
|
||||||
.get_tenant_by_stripe_customer_id(customer_id)
|
.get_tenant_by_stripe_customer_id(customer_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| anyhow!("tenant not found for customer"))?;
|
||||||
InvoiceLookupError::Internal(anyhow!("tenant not found for customer"))
|
Ok(Some((invoice, tenant)))
|
||||||
})?;
|
|
||||||
Ok((invoice, tenant))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn reconcile_manual_lightning_invoice(
|
pub async fn reconcile_manual_lightning_invoice(
|
||||||
&self,
|
&self,
|
||||||
invoice_id: &str,
|
invoice_id: &str,
|
||||||
invoice: &serde_json::Value,
|
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)
|
self.reconcile_manual_lightning_invoice_if_settled(invoice_id, invoice)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -853,7 +854,7 @@ impl Billing {
|
|||||||
&self,
|
&self,
|
||||||
invoice_id: &str,
|
invoice_id: &str,
|
||||||
invoice: &serde_json::Value,
|
invoice: &serde_json::Value,
|
||||||
) -> std::result::Result<serde_json::Value, InvoiceLookupError> {
|
) -> Result<serde_json::Value> {
|
||||||
if invoice["status"].as_str().unwrap_or_default() != "open" {
|
if invoice["status"].as_str().unwrap_or_default() != "open" {
|
||||||
return Ok(invoice.clone());
|
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> {
|
async fn is_manual_lightning_invoice_settled(&self, bolt11: &str) -> Result<bool> {
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::extract::{Path, State};
|
use axum::extract::{Path, State};
|
||||||
use reqwest::StatusCode;
|
|
||||||
|
|
||||||
use crate::api::{Api, AuthedPubkey};
|
use crate::api::{Api, AuthedPubkey};
|
||||||
use crate::stripe::InvoiceLookupError;
|
use crate::web::{ApiResult, bad_request, internal, not_found, ok};
|
||||||
use crate::web::{ApiError, ApiResult, bad_request, internal, not_found, ok};
|
|
||||||
|
|
||||||
pub async fn list_tenant_invoices(
|
pub async fn list_tenant_invoices(
|
||||||
State(api): State<Arc<Api>>,
|
State(api): State<Arc<Api>>,
|
||||||
@@ -32,14 +30,15 @@ pub async fn get_invoice(
|
|||||||
.billing
|
.billing
|
||||||
.get_invoice_with_tenant(&id)
|
.get_invoice_with_tenant(&id)
|
||||||
.await
|
.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)?;
|
api.require_admin_or_tenant(&auth, &tenant.pubkey)?;
|
||||||
|
|
||||||
let invoice = api
|
let invoice = api
|
||||||
.billing
|
.billing
|
||||||
.reconcile_manual_lightning_invoice(&id, &invoice)
|
.reconcile_manual_lightning_invoice(&id, &invoice)
|
||||||
.await
|
.await
|
||||||
.map_err(map_invoice_lookup_error)?;
|
.map_err(internal)?;
|
||||||
|
|
||||||
ok(invoice)
|
ok(invoice)
|
||||||
}
|
}
|
||||||
@@ -53,14 +52,15 @@ pub async fn get_invoice_bolt11(
|
|||||||
.billing
|
.billing
|
||||||
.get_invoice_with_tenant(&id)
|
.get_invoice_with_tenant(&id)
|
||||||
.await
|
.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)?;
|
api.require_admin_or_tenant(&auth, &tenant.pubkey)?;
|
||||||
|
|
||||||
let invoice = api
|
let invoice = api
|
||||||
.billing
|
.billing
|
||||||
.reconcile_manual_lightning_invoice(&id, &invoice)
|
.reconcile_manual_lightning_invoice(&id, &invoice)
|
||||||
.await
|
.await
|
||||||
.map_err(map_invoice_lookup_error)?;
|
.map_err(internal)?;
|
||||||
|
|
||||||
let status = invoice["status"].as_str().unwrap_or_default();
|
let status = invoice["status"].as_str().unwrap_or_default();
|
||||||
if status != "open" {
|
if status != "open" {
|
||||||
@@ -77,16 +77,3 @@ pub async fn get_invoice_bolt11(
|
|||||||
.map_err(internal)?;
|
.map_err(internal)?;
|
||||||
ok(serde_json::json!({ "bolt11": bolt11 }))
|
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 STRIPE_API: &str = "https://api.stripe.com/v1";
|
||||||
const WEBHOOK_TOLERANCE_SECS: i64 = 300;
|
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.
|
/// A Stripe webhook event with its signature already verified.
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct Event {
|
pub struct Event {
|
||||||
@@ -263,23 +225,20 @@ impl Stripe {
|
|||||||
Ok(body["data"].clone())
|
Ok(body["data"].clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_invoice(
|
/// Fetches an invoice, returning `None` if Stripe no longer knows about it
|
||||||
&self,
|
/// (so callers can surface a 404 to the user).
|
||||||
invoice_id: &str,
|
pub async fn get_invoice(&self, invoice_id: &str) -> Result<Option<serde_json::Value>> {
|
||||||
) -> std::result::Result<serde_json::Value, InvoiceLookupError> {
|
|
||||||
let resp = self
|
let resp = self
|
||||||
.http
|
.http
|
||||||
.get(format!("{STRIPE_API}/invoices/{invoice_id}"))
|
.get(format!("{STRIPE_API}/invoices/{invoice_id}"))
|
||||||
.bearer_auth(&self.secret_key)
|
.bearer_auth(&self.secret_key)
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
if resp.status().is_client_error() {
|
if resp.status() == reqwest::StatusCode::NOT_FOUND {
|
||||||
return Err(InvoiceLookupError::StripeClient {
|
return Ok(None);
|
||||||
status: resp.status(),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
let body: serde_json::Value = error_for_status(resp).await?.json().await?;
|
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<()> {
|
pub async fn pay_invoice(&self, invoice_id: &str) -> Result<()> {
|
||||||
|
|||||||
Reference in New Issue
Block a user