Add endpoint for paying an invoice so that users don't get expired qr codes
Docker / build-and-push-image (backend, backend, coracle/caravel-backend) (push) Failing after 1s
Docker / build-and-push-image (frontend, frontend, coracle/caravel-frontend) (push) Failing after 0s

This commit is contained in:
Jon Staab
2026-05-25 10:51:19 -07:00
parent 384ddbd439
commit 1f99d0bd34
7 changed files with 96 additions and 32 deletions
+1
View File
@@ -3,6 +3,7 @@ SERVER_HOST=127.0.0.1
SERVER_PORT=2892
SERVER_ALLOW_ORIGINS= # Optional comma-separated allowed CORS origins; empty = permissive
SERVER_ADMIN_PUBKEYS= # Comma-separated hex pubkeys with admin access
APP_URL=http://127.0.0.1:5173 # Public base URL of the frontend, used for links in billing DMs
# Database
DATABASE_URL=sqlite://data/caravel.db
+1 -1
View File
@@ -219,7 +219,7 @@ Attempts to pay a new subscription invoice (Stripe is pay-in-advance, so this fi
1. **NWC auto-pay**: if the tenant has a `nwc_url`, run `billing.pay_invoice_nwc`. On success, done. On failure, record the error via `command.set_tenant_nwc_error`, log it, summarize it for the eventual DM, and fall through.
2. **Card on file**: if `stripe.has_payment_method`, do nothing — Stripe charges automatically for this attempt.
3. **Manual payment**: send a DM via `robot.send_dm` telling the tenant payment is due (including the summarized NWC error, if any), with a link to the app for manual Lightning payment.
3. **Manual payment**: send a DM via `robot.send_dm` telling the tenant payment is due (including the summarized NWC error, if any). The DM includes a link to the dashboard payment view for this invoice — `{env.app_url}/account?invoice={stripe_invoice_id}` — where the tenant can review the invoice and pay by Lightning or card.
## `invoice.paid`
+1
View File
@@ -8,6 +8,7 @@ Members (all populated from environment variables):
- `server_port: u16` - from `SERVER_PORT`
- `server_admin_pubkeys: Vec<String>` - admin pubkeys from `SERVER_ADMIN_PUBKEYS`
- `server_allow_origins: Vec<String>` - CORS origins from `SERVER_ALLOW_ORIGINS`
- `app_url: String` - public base URL of the frontend app from `APP_URL`, with any trailing slash stripped; used to build tenant-facing links such as the invoice payment link in billing DMs
- `database_url: String` - from `DATABASE_URL`
- `robot_name: String` - from `ROBOT_NAME`
- `robot_wallet: String` - the system NWC URL from `ROBOT_WALLET`, used to issue/look up bolt11 invoices
+4
View File
@@ -7,6 +7,9 @@ pub struct Env {
pub server_port: u16,
pub server_admin_pubkeys: Vec<String>,
pub server_allow_origins: Vec<String>,
/// Public base URL of the frontend app, used to build links sent to tenants
/// (e.g. the invoice payment link in billing DMs). No trailing slash.
pub app_url: String,
pub database_url: String,
pub robot_name: String,
pub robot_wallet: String,
@@ -43,6 +46,7 @@ impl Env {
server_port: require_u16("SERVER_PORT"),
server_admin_pubkeys: require_csv("SERVER_ADMIN_PUBKEYS"),
server_allow_origins: require_csv("SERVER_ALLOW_ORIGINS"),
app_url: require_str("APP_URL").trim_end_matches('/').to_string(),
database_url: require_str("DATABASE_URL"),
robot_name: require_str("ROBOT_NAME"),
robot_wallet: require_str("ROBOT_WALLET"),
+11 -12
View File
@@ -12,7 +12,7 @@ use crate::api::{Api, AuthedPubkey};
use crate::models::{RELAY_STATUS_ACTIVE, RELAY_STATUS_DELINQUENT};
use crate::web::{ApiResult, bad_request, internal, ok};
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. Open the link below to review the invoice and pay by Lightning or card:";
const NWC_ERROR_DM_PREFIX: &str = "NWC auto-payment failed:";
const NWC_ERROR_DM_MAX_CHARS: usize = 240;
@@ -165,8 +165,16 @@ async fn handle_invoice_created(
return Ok(());
}
// 3. Manual payment: send a DM
let dm_message = manual_lightning_payment_dm(nwc_error_for_dm.as_deref());
// 3. Manual payment: DM a link to the in-app payment page for this invoice
let url_base = &api.env.app_url;
let payment_url = format!("{url_base}/account?invoice={stripe_invoice_id}");
let base = format!("{MANUAL_LIGHTNING_PAYMENT_DM}\n\n{payment_url}");
let dm_message = match nwc_error_for_dm {
Some(error) if !error.is_empty() => {
format!("{base}\n\n{NWC_ERROR_DM_PREFIX} {error}")
}
_ => base,
};
api.robot.send_dm(&tenant.pubkey, &dm_message).await?;
Ok(())
@@ -339,12 +347,3 @@ fn summarize_nwc_error_for_dm(error: &str) -> Option<String> {
truncated.push_str("...");
Some(truncated)
}
fn manual_lightning_payment_dm(nwc_error: Option<&str>) -> String {
match nwc_error {
Some(error) if !error.is_empty() => {
format!("{MANUAL_LIGHTNING_PAYMENT_DM}\n\n{NWC_ERROR_DM_PREFIX} {error}")
}
_ => MANUAL_LIGHTNING_PAYMENT_DM.to_string(),
}
}