Fix a few bugs
Docker / build-and-push-image (push) Successful in 1h6m37s

This commit is contained in:
Jon Staab
2026-06-05 11:15:46 -07:00
parent 791a4fcb70
commit b5f3efc775
6 changed files with 61 additions and 56 deletions
+3
View File
@@ -42,6 +42,7 @@ CREATE TABLE IF NOT EXISTS relay (
blossom_enabled INTEGER NOT NULL DEFAULT 0,
livekit_enabled INTEGER NOT NULL DEFAULT 0,
push_enabled INTEGER NOT NULL DEFAULT 1,
created_at INTEGER NOT NULL,
FOREIGN KEY (tenant_pubkey) REFERENCES tenant(pubkey)
);
@@ -114,6 +115,8 @@ CREATE INDEX IF NOT EXISTS idx_activity_unbilled ON activity (tenant_pubkey, cre
CREATE INDEX IF NOT EXISTS idx_relay_tenant_pubkey ON relay (tenant_pubkey);
CREATE INDEX IF NOT EXISTS idx_relay_created ON relay (created_at);
CREATE INDEX IF NOT EXISTS idx_invoice_tenant_created ON invoice (tenant_pubkey, created_at);
-- Dunning scans a tenant's still-open invoices oldest-first to retry payment.
+28 -41
View File
@@ -352,8 +352,6 @@ impl Billing {
autopay: bool,
notify: bool,
) -> Result<()> {
let mut error_message: Option<String> = None;
// 1. Out-of-band lightning: settle a bolt11 paid out of band (e.g. a
// manual QR scan, or an NWC pay that completed but failed to record).
// Checked before any charge so we never bill on top of it.
@@ -385,23 +383,24 @@ impl Billing {
}
// 3. NWC auto-pay: if the tenant has configured an nwc_url, charge it.
if !tenant.nwc_url.is_empty() {
match self.attempt_payment_using_nwc(tenant, invoice).await {
Ok(()) => return self.cleanup_pending_payments(invoice).await,
Err(e) => error_message = Some(format!("{e}")),
}
if !tenant.nwc_url.is_empty()
&& self
.attempt_payment_using_nwc(tenant, invoice)
.await
.is_ok()
{
return self.cleanup_pending_payments(invoice).await;
}
// 4. Payment method on file: charge the tenant's cached Stripe payment
// method, kept fresh by sync_stripe_payment_method before collection.
if let Some(payment_method) = &tenant.stripe_payment_method_id {
match self
if let Some(payment_method) = &tenant.stripe_payment_method_id
&& self
.attempt_payment_using_stripe(tenant, invoice, payment_method)
.await
{
Ok(()) => return self.cleanup_pending_payments(invoice).await,
Err(e) => error_message = error_message.or_else(|| Some(format!("{e}"))),
}
.is_ok()
{
return self.cleanup_pending_payments(invoice).await;
}
if !notify {
@@ -409,10 +408,7 @@ impl Billing {
}
// 5. Manual payment: DM a link to the in-app payment page for this invoice.
if let Err(e) = self
.attempt_payment_using_dm(tenant, invoice, error_message)
.await
{
if let Err(e) = self.attempt_payment_using_dm(tenant, invoice).await {
tracing::error!(
tenant = %tenant.pubkey,
error = %e,
@@ -482,12 +478,7 @@ impl Billing {
.await
}
async fn attempt_payment_using_dm(
&self,
tenant: &Tenant,
invoice: &Invoice,
error: Option<String>,
) -> Result<()> {
async fn attempt_payment_using_dm(&self, tenant: &Tenant, invoice: &Invoice) -> Result<()> {
let now = chrono::Utc::now().timestamp();
// If the invoice was just generated, give the user a chance to check the dashboard before nagging them.
@@ -507,21 +498,10 @@ impl Billing {
let invoice_id = &invoice.id;
let url_base = &env::get().app_url;
let payment_url = format!("{url_base}/account?invoice={invoice_id}");
let base = format!("{MANUAL_PAYMENT_DM}\n\n{payment_url}");
let dm_message = match error {
Some(error) if !error.is_empty() => {
let limit: usize = 240;
let summary = error
.chars()
.take(limit.saturating_sub(3))
.collect::<String>();
format!("{base}\n\nAuto-payment failed: {summary}")
}
_ => base,
};
let message = format!("{MANUAL_PAYMENT_DM}\n\n{payment_url}");
// Send via NIP 17
self.robot.send_dm(&tenant.pubkey, &dm_message).await?;
self.robot.send_dm(&tenant.pubkey, &message).await?;
// Record the send to avoid spammy notifications.
command::mark_invoice_notified(invoice_id).await
@@ -534,7 +514,11 @@ impl Billing {
/// transaction, so it's excluded here and not needlessly expired.
async fn cleanup_pending_payments(&self, invoice: &Invoice) -> Result<()> {
for checkout in query::list_pending_checkouts_for_invoice(&invoice.id).await? {
if let Err(error) = self.stripe.expire_checkout_session(&checkout.session_id).await {
if let Err(error) = self
.stripe
.expire_checkout_session(&checkout.session_id)
.await
{
tracing::debug!(
invoice = %invoice.id,
checkout = %checkout.id,
@@ -561,7 +545,8 @@ impl Billing {
}
if let Some(existing) = query::get_bolt11_for_invoice(&invoice.id).await?
&& (existing.settled_at.is_none() || now < existing.expires_at)
&& existing.settled_at.is_none()
&& now < existing.expires_at
{
return Ok(existing);
}
@@ -713,15 +698,17 @@ impl BillingPeriod {
/// Fraction of this period still unused at `at`, in `[0.0, 1.0]`, for
/// prorating a mid-period charge or credit. The remaining time is rounded to
/// the nearest hour so proration tracks whole hours rather than exact seconds.
/// the nearest day so proration tracks whole days rather than exact seconds.
/// Rounding by the day keeps a relay created within the first half-day of a
/// period billed at the full plan amount instead of a few cents short.
fn fraction_remaining(&self, at: i64) -> f64 {
const HOUR: i64 = 60 * 60;
const DAY: i64 = 24 * 60 * 60;
let len = (self.end - self.start) as f64;
if len <= 0.0 {
return 1.0;
}
let remaining = ((self.end - at) + HOUR / 2) / HOUR * HOUR;
let remaining = ((self.end - at) + DAY / 2) / DAY * DAY;
(remaining as f64 / len).clamp(0.0, 1.0)
}
+4 -2
View File
@@ -146,6 +146,7 @@ pub async fn reactivate_tenant(tenant_pubkey: &str, relays: &[Relay]) -> Result<
// --- Relays ---
pub async fn create_relay(relay: &Relay) -> Result<()> {
let created_at = chrono::Utc::now().timestamp();
let activity = with_tx(async |tx| {
sqlx::query(
"INSERT INTO relay (
@@ -153,8 +154,8 @@ pub async fn create_relay(relay: &Relay) -> Result<()> {
info_name, info_icon, info_description,
policy_public_join, policy_strip_signatures,
groups_enabled, management_enabled, blossom_enabled,
livekit_enabled, push_enabled
) VALUES (?, ?, ?, ?, 'active', 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
livekit_enabled, push_enabled, created_at
) VALUES (?, ?, ?, ?, 'active', 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
)
.bind(&relay.id)
.bind(&relay.tenant_pubkey)
@@ -171,6 +172,7 @@ pub async fn create_relay(relay: &Relay) -> Result<()> {
.bind(relay.blossom_enabled)
.bind(relay.livekit_enabled)
.bind(relay.push_enabled)
.bind(created_at)
.execute(&mut **tx)
.await?;
let snapshot = Snapshot::Relay {
+2
View File
@@ -91,6 +91,7 @@ pub struct Relay {
pub livekit_enabled: i64,
pub push_enabled: i64,
pub synced: i64,
pub created_at: i64,
}
impl Default for Relay {
@@ -113,6 +114,7 @@ impl Default for Relay {
livekit_enabled: 0,
push_enabled: 1,
synced: 0,
created_at: 0,
}
}
}
+16 -12
View File
@@ -56,9 +56,11 @@ pub fn get_plan(plan_id: &str) -> Result<Plan> {
// --- Tenants ---
pub async fn list_tenants() -> Result<Vec<Tenant>> {
Ok(sqlx::query_as::<_, Tenant>(&select_tenant(""))
.fetch_all(pool())
.await?)
Ok(
sqlx::query_as::<_, Tenant>(&select_tenant("ORDER BY created_at DESC"))
.fetch_all(pool())
.await?,
)
}
pub async fn get_tenant(pubkey: &str) -> Result<Option<Tenant>> {
@@ -73,9 +75,11 @@ pub async fn get_tenant(pubkey: &str) -> Result<Option<Tenant>> {
// --- Relays ---
pub async fn list_relays() -> Result<Vec<Relay>> {
Ok(sqlx::query_as::<_, Relay>(&select_relay(""))
.fetch_all(pool())
.await?)
Ok(
sqlx::query_as::<_, Relay>(&select_relay("ORDER BY created_at DESC"))
.fetch_all(pool())
.await?,
)
}
pub async fn list_relays_pending_sync() -> Result<Vec<Relay>> {
@@ -87,12 +91,12 @@ pub async fn list_relays_pending_sync() -> Result<Vec<Relay>> {
}
pub async fn list_relays_for_tenant(tenant_pubkey: &str) -> Result<Vec<Relay>> {
Ok(
sqlx::query_as::<_, Relay>(&select_relay("WHERE tenant_pubkey = ?"))
.bind(tenant_pubkey)
.fetch_all(pool())
.await?,
)
Ok(sqlx::query_as::<_, Relay>(&select_relay(
"WHERE tenant_pubkey = ? ORDER BY created_at DESC",
))
.bind(tenant_pubkey)
.fetch_all(pool())
.await?)
}
pub async fn get_relay(id: &str) -> Result<Option<Relay>> {