Rename tenant fields to tenant_pubkey and plan to plan_id

This commit is contained in:
Jon Staab
2026-05-28 15:18:41 -07:00
parent 9f599d66be
commit eb0123abef
18 changed files with 148 additions and 148 deletions
+21 -19
View File
@@ -1,14 +1,3 @@
CREATE TABLE IF NOT EXISTS activity (
id TEXT PRIMARY KEY,
tenant TEXT NOT NULL,
created_at INTEGER NOT NULL,
activity_type TEXT NOT NULL,
resource_type TEXT NOT NULL,
resource_id TEXT NOT NULL,
billed_at INTEGER,
plan_id TEXT
);
CREATE TABLE IF NOT EXISTS tenant ( CREATE TABLE IF NOT EXISTS tenant (
pubkey TEXT PRIMARY KEY, pubkey TEXT PRIMARY KEY,
nwc_url TEXT NOT NULL DEFAULT '', nwc_url TEXT NOT NULL DEFAULT '',
@@ -19,11 +8,24 @@ CREATE TABLE IF NOT EXISTS tenant (
renewed_at INTEGER renewed_at INTEGER
); );
CREATE TABLE IF NOT EXISTS activity (
id TEXT PRIMARY KEY,
tenant_pubkey TEXT NOT NULL,
created_at INTEGER NOT NULL,
activity_type TEXT NOT NULL,
resource_type TEXT NOT NULL,
resource_id TEXT NOT NULL,
billed_at INTEGER,
plan_id TEXT,
FOREIGN KEY (tenant_pubkey) REFERENCES tenant(pubkey)
);
CREATE TABLE IF NOT EXISTS relay ( CREATE TABLE IF NOT EXISTS relay (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
tenant TEXT NOT NULL, tenant_pubkey TEXT NOT NULL,
subdomain TEXT NOT NULL UNIQUE, subdomain TEXT NOT NULL UNIQUE,
plan TEXT NOT NULL, plan_id TEXT NOT NULL,
status TEXT NOT NULL, status TEXT NOT NULL,
synced INTEGER NOT NULL DEFAULT 0, synced INTEGER NOT NULL DEFAULT 0,
sync_error TEXT NOT NULL DEFAULT '', sync_error TEXT NOT NULL DEFAULT '',
@@ -37,7 +39,7 @@ CREATE TABLE IF NOT EXISTS relay (
blossom_enabled INTEGER NOT NULL DEFAULT 0, blossom_enabled INTEGER NOT NULL DEFAULT 0,
livekit_enabled INTEGER NOT NULL DEFAULT 0, livekit_enabled INTEGER NOT NULL DEFAULT 0,
push_enabled INTEGER NOT NULL DEFAULT 1, push_enabled INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY (tenant) REFERENCES tenant(pubkey) FOREIGN KEY (tenant_pubkey) REFERENCES tenant(pubkey)
); );
CREATE TABLE IF NOT EXISTS invoice ( CREATE TABLE IF NOT EXISTS invoice (
@@ -58,7 +60,7 @@ CREATE TABLE IF NOT EXISTS invoice_item (
activity_id TEXT, activity_id TEXT,
tenant_pubkey TEXT NOT NULL, tenant_pubkey TEXT NOT NULL,
relay_id TEXT NOT NULL, relay_id TEXT NOT NULL,
plan TEXT NOT NULL, plan_id TEXT NOT NULL,
amount INTEGER NOT NULL, amount INTEGER NOT NULL,
description TEXT NOT NULL DEFAULT '', description TEXT NOT NULL DEFAULT '',
created_at INTEGER NOT NULL, created_at INTEGER NOT NULL,
@@ -84,13 +86,13 @@ CREATE TABLE IF NOT EXISTS intent (
FOREIGN KEY (invoice_id) REFERENCES invoice(id) FOREIGN KEY (invoice_id) REFERENCES invoice(id)
); );
CREATE INDEX IF NOT EXISTS idx_relay_tenant ON relay (tenant); CREATE INDEX IF NOT EXISTS idx_activity_tenant_created ON activity (tenant_pubkey, created_at);
CREATE INDEX IF NOT EXISTS idx_activity_tenant_created ON activity (tenant, created_at);
CREATE INDEX IF NOT EXISTS idx_activity_resource_created ON activity (resource_id, created_at); CREATE INDEX IF NOT EXISTS idx_activity_resource_created ON activity (resource_id, created_at);
CREATE INDEX IF NOT EXISTS idx_activity_unbilled ON activity (tenant, created_at) WHERE billed_at IS NULL; CREATE INDEX IF NOT EXISTS idx_activity_unbilled ON activity (tenant_pubkey, created_at) WHERE billed_at IS NULL;
CREATE INDEX IF NOT EXISTS idx_relay_tenant_pubkey ON relay (tenant_pubkey);
CREATE INDEX IF NOT EXISTS idx_invoice_tenant_created ON invoice (tenant_pubkey, created_at); CREATE INDEX IF NOT EXISTS idx_invoice_tenant_created ON invoice (tenant_pubkey, created_at);
+21 -23
View File
@@ -79,7 +79,7 @@ impl Billing {
command::set_tenant_billing_anchor(&tenant).await?; command::set_tenant_billing_anchor(&tenant).await?;
} }
let invoice_item = self.reconcile_activity(&tenant, &activity).await?; self.reconcile_activity(&tenant, &activity).await?;
} }
// If the tenant has no billing anchor, they have nothing to bill // If the tenant has no billing anchor, they have nothing to bill
@@ -104,7 +104,7 @@ impl Billing {
/// persist it with the activity's billed marker. Activities that produce no /// persist it with the activity's billed marker. Activities that produce no
/// item (e.g. free-plan changes) are still marked billed so they aren't /// item (e.g. free-plan changes) are still marked billed so they aren't
/// re-scanned. /// re-scanned.
async fn reconcile_activity(&self, tenant: &Tenant, activity: &Activity) -> Result<Option<InvoiceItem>> { async fn reconcile_activity(&self, tenant: &Tenant, activity: &Activity) -> Result<()> {
let invoice_item = match activity.activity_type.as_str() { let invoice_item = match activity.activity_type.as_str() {
"create_relay" => { "create_relay" => {
self.make_prorated_item(tenant, activity, 1, "New relay created") self.make_prorated_item(tenant, activity, 1, "New relay created")
@@ -123,11 +123,9 @@ impl Billing {
}; };
match invoice_item { match invoice_item {
Some(ref item) => command::insert_invoice_item_for_activity(&item, &activity.id).await?, Some(ref item) => command::insert_invoice_item_for_activity(&item, &activity.id).await,
None => command::mark_activity_billed(&activity.id).await?, None => command::mark_activity_billed(&activity.id).await,
} }
Ok(invoice_item)
} }
/// A prorated charge (or credit, with `sign` = -1) for the relay's current /// A prorated charge (or credit, with `sign` = -1) for the relay's current
@@ -141,10 +139,10 @@ impl Billing {
description: &str, description: &str,
) -> Result<Option<InvoiceItem>> { ) -> Result<Option<InvoiceItem>> {
let Some(relay) = query::get_relay(&activity.resource_id).await? else { let Some(relay) = query::get_relay(&activity.resource_id).await? else {
return anyhow!("activity resource was not a valid relay"); return Err(anyhow!("activity resource was not a valid relay"));
}; };
let Some(plan) = query::get_plan(&relay.plan) else { let Some(plan) = query::get_plan(&relay.plan_id) else {
return anyhow!("activity plan was not a valid plan"); return Err(anyhow!("activity plan was not a valid plan"));
}; };
if plan.amount <= 0 { if plan.amount <= 0 {
return Ok(None); return Ok(None);
@@ -158,9 +156,9 @@ impl Billing {
id: uuid::Uuid::new_v4().to_string(), id: uuid::Uuid::new_v4().to_string(),
invoice_id: None, invoice_id: None,
activity_id: Some(activity.id.clone()), activity_id: Some(activity.id.clone()),
tenant_pubkey: activity.tenant.clone(), tenant_pubkey: activity.tenant_pubkey.clone(),
relay_id: activity.resource_id.clone(), relay_id: activity.resource_id.clone(),
plan: plan.id, plan_id: plan.id,
amount, amount,
description: description.to_string(), description: description.to_string(),
created_at: activity.created_at, created_at: activity.created_at,
@@ -178,21 +176,21 @@ impl Billing {
activity: &Activity, activity: &Activity,
) -> Result<Option<InvoiceItem>> { ) -> Result<Option<InvoiceItem>> {
let Some(new_plan_id) = activity.plan_id.as_deref() else { let Some(new_plan_id) = activity.plan_id.as_deref() else {
return anyhow!("activity plan was not a valid plan"); return Err(anyhow!("activity plan was not a valid plan"));
}; };
let Some(old_plan_id) = let Some(old_plan_id) =
query::get_relay_plan_before(&activity.resource_id, activity.created_at).await? query::get_relay_plan_before(&activity.resource_id, activity.created_at).await?
else { else {
return anyhow!("no previous plan found for relay update activity"); return Err(anyhow!("no previous plan found for relay update activity"));
}; };
if old_plan_id == new_plan_id { if old_plan_id == new_plan_id {
return Ok(None); return Ok(None);
} }
let Some(new_plan) = query::get_plan(new_plan_id) else { let Some(new_plan) = query::get_plan(new_plan_id) else {
return anyhow!("new plan is an invalid plan"); return Err(anyhow!("new plan is an invalid plan"));
}; };
let Some(old_plan) = query::get_plan(old_plan_id) else { let Some(old_plan) = query::get_plan(&old_plan_id) else {
return anyhow!("old plan is an invalid plan"); return Err(anyhow!("old plan is an invalid plan"));
}; };
let period = BillingPeriod::at(tenant, activity.created_at) let period = BillingPeriod::at(tenant, activity.created_at)
@@ -208,9 +206,9 @@ impl Billing {
id: uuid::Uuid::new_v4().to_string(), id: uuid::Uuid::new_v4().to_string(),
invoice_id: None, invoice_id: None,
activity_id: Some(activity.id.clone()), activity_id: Some(activity.id.clone()),
tenant_pubkey: activity.tenant.clone(), tenant_pubkey: activity.tenant_pubkey.clone(),
relay_id: activity.resource_id.clone(), relay_id: activity.resource_id.clone(),
plan: new_plan.id, plan_id: new_plan.id,
amount, amount,
description, description,
created_at: activity.created_at, created_at: activity.created_at,
@@ -232,7 +230,7 @@ impl Billing {
if !state.active { if !state.active {
continue; continue;
} }
let Some(plan) = state.plan.and_then(|id| query::get_plan(&id)) else { let Some(plan) = state.plan_id.and_then(|id| query::get_plan(&id)) else {
continue; continue;
}; };
if plan.amount <= 0 { if plan.amount <= 0 {
@@ -244,7 +242,7 @@ impl Billing {
activity_id: None, activity_id: None,
tenant_pubkey: tenant.pubkey.clone(), tenant_pubkey: tenant.pubkey.clone(),
relay_id, relay_id,
plan: plan.id, plan_id: plan.id,
amount: plan.amount, amount: plan.amount,
description: "Subscription renewal".to_string(), description: "Subscription renewal".to_string(),
created_at: period.start, created_at: period.start,
@@ -483,7 +481,7 @@ impl BillingPeriod {
#[derive(Default)] #[derive(Default)]
struct RelayState { struct RelayState {
active: bool, active: bool,
plan: Option<String>, plan_id: Option<String>,
} }
/// Fold relay activities (which must be oldest-first) into each relay's /// Fold relay activities (which must be oldest-first) into each relay's
@@ -498,11 +496,11 @@ fn relay_states(activities: &[Activity]) -> HashMap<String, RelayState> {
match activity.activity_type.as_str() { match activity.activity_type.as_str() {
"create_relay" => { "create_relay" => {
state.active = true; state.active = true;
state.plan = activity.plan_id.clone(); state.plan_id = activity.plan_id.clone();
} }
"update_relay" => { "update_relay" => {
if activity.plan_id.is_some() { if activity.plan_id.is_some() {
state.plan = activity.plan_id.clone(); state.plan_id = activity.plan_id.clone();
} }
} }
"activate_relay" => state.active = true, "activate_relay" => state.active = true,
+15 -15
View File
@@ -66,7 +66,7 @@ pub async fn create_relay(relay: &Relay) -> Result<()> {
let activity = with_tx(async |tx| { let activity = with_tx(async |tx| {
sqlx::query( sqlx::query(
"INSERT INTO relay ( "INSERT INTO relay (
id, tenant, subdomain, plan, status, synced, sync_error, id, tenant_pubkey, subdomain, plan_id, status, synced, sync_error,
info_name, info_icon, info_description, info_name, info_icon, info_description,
policy_public_join, policy_strip_signatures, policy_public_join, policy_strip_signatures,
groups_enabled, management_enabled, blossom_enabled, groups_enabled, management_enabled, blossom_enabled,
@@ -74,9 +74,9 @@ pub async fn create_relay(relay: &Relay) -> Result<()> {
) VALUES (?, ?, ?, ?, 'active', 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", ) VALUES (?, ?, ?, ?, 'active', 0, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
) )
.bind(&relay.id) .bind(&relay.id)
.bind(&relay.tenant) .bind(&relay.tenant_pubkey)
.bind(&relay.subdomain) .bind(&relay.subdomain)
.bind(&relay.plan) .bind(&relay.plan_id)
.bind(&relay.sync_error) .bind(&relay.sync_error)
.bind(&relay.info_name) .bind(&relay.info_name)
.bind(&relay.info_icon) .bind(&relay.info_icon)
@@ -90,7 +90,7 @@ pub async fn create_relay(relay: &Relay) -> Result<()> {
.bind(relay.push_enabled) .bind(relay.push_enabled)
.execute(&mut **tx) .execute(&mut **tx)
.await?; .await?;
insert_activity_tx(tx, "create_relay", "relay", &relay.id, Some(&relay.plan)).await insert_activity_tx(tx, "create_relay", "relay", &relay.id, Some(&relay.plan_id)).await
}) })
.await?; .await?;
publish(activity); publish(activity);
@@ -101,16 +101,16 @@ pub async fn update_relay(relay: &Relay) -> Result<()> {
let activity = with_tx(async |tx| { let activity = with_tx(async |tx| {
sqlx::query( sqlx::query(
"UPDATE relay "UPDATE relay
SET tenant = ?, subdomain = ?, plan = ?, status = ?, sync_error = ?, synced = 0, SET tenant_pubkey = ?, subdomain = ?, plan_id = ?, status = ?, sync_error = ?, synced = 0,
info_name = ?, info_icon = ?, info_description = ?, info_name = ?, info_icon = ?, info_description = ?,
policy_public_join = ?, policy_strip_signatures = ?, policy_public_join = ?, policy_strip_signatures = ?,
groups_enabled = ?, management_enabled = ?, blossom_enabled = ?, groups_enabled = ?, management_enabled = ?, blossom_enabled = ?,
livekit_enabled = ?, push_enabled = ? livekit_enabled = ?, push_enabled = ?
WHERE id = ?", WHERE id = ?",
) )
.bind(&relay.tenant) .bind(&relay.tenant_pubkey)
.bind(&relay.subdomain) .bind(&relay.subdomain)
.bind(&relay.plan) .bind(&relay.plan_id)
.bind(&relay.status) .bind(&relay.status)
.bind(&relay.sync_error) .bind(&relay.sync_error)
.bind(&relay.info_name) .bind(&relay.info_name)
@@ -126,7 +126,7 @@ pub async fn update_relay(relay: &Relay) -> Result<()> {
.bind(&relay.id) .bind(&relay.id)
.execute(&mut **tx) .execute(&mut **tx)
.await?; .await?;
insert_activity_tx(tx, "update_relay", "relay", &relay.id, Some(&relay.plan)).await insert_activity_tx(tx, "update_relay", "relay", &relay.id, Some(&relay.plan_id)).await
}) })
.await?; .await?;
publish(activity); publish(activity);
@@ -376,10 +376,10 @@ async fn insert_activity_tx(
resource_id: &str, resource_id: &str,
plan_id: Option<&str>, plan_id: Option<&str>,
) -> Result<Activity> { ) -> Result<Activity> {
let tenant = match resource_type { let tenant_pubkey = match resource_type {
"tenant" => resource_id.to_string(), "tenant" => resource_id.to_string(),
"relay" => { "relay" => {
sqlx::query_scalar::<_, String>("SELECT tenant FROM relay WHERE id = ?") sqlx::query_scalar::<_, String>("SELECT tenant_pubkey FROM relay WHERE id = ?")
.bind(resource_id) .bind(resource_id)
.fetch_one(&mut **tx) .fetch_one(&mut **tx)
.await? .await?
@@ -391,11 +391,11 @@ async fn insert_activity_tx(
let created_at = chrono::Utc::now().timestamp(); let created_at = chrono::Utc::now().timestamp();
sqlx::query( sqlx::query(
"INSERT INTO activity (id, tenant, created_at, activity_type, resource_type, resource_id, plan_id) "INSERT INTO activity (id, tenant_pubkey, created_at, activity_type, resource_type, resource_id, plan_id)
VALUES (?, ?, ?, ?, ?, ?, ?)", VALUES (?, ?, ?, ?, ?, ?, ?)",
) )
.bind(&id) .bind(&id)
.bind(&tenant) .bind(&tenant_pubkey)
.bind(created_at) .bind(created_at)
.bind(activity_type) .bind(activity_type)
.bind(resource_type) .bind(resource_type)
@@ -406,7 +406,7 @@ async fn insert_activity_tx(
Ok(Activity { Ok(Activity {
id, id,
tenant, tenant_pubkey,
created_at, created_at,
activity_type: activity_type.to_string(), activity_type: activity_type.to_string(),
resource_type: resource_type.to_string(), resource_type: resource_type.to_string(),
@@ -441,7 +441,7 @@ async fn insert_invoice_tx(
async fn insert_invoice_item_tx(tx: &mut Transaction<'_, Sqlite>, item: &InvoiceItem) -> Result<()> { async fn insert_invoice_item_tx(tx: &mut Transaction<'_, Sqlite>, item: &InvoiceItem) -> Result<()> {
sqlx::query( sqlx::query(
"INSERT INTO invoice_item "INSERT INTO invoice_item
(id, invoice_id, activity_id, tenant_pubkey, relay_id, plan, amount, description, created_at) (id, invoice_id, activity_id, tenant_pubkey, relay_id, plan_id, amount, description, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
) )
.bind(&item.id) .bind(&item.id)
@@ -449,7 +449,7 @@ async fn insert_invoice_item_tx(tx: &mut Transaction<'_, Sqlite>, item: &Invoice
.bind(&item.activity_id) .bind(&item.activity_id)
.bind(&item.tenant_pubkey) .bind(&item.tenant_pubkey)
.bind(&item.relay_id) .bind(&item.relay_id)
.bind(&item.plan) .bind(&item.plan_id)
.bind(item.amount) .bind(item.amount)
.bind(&item.description) .bind(&item.description)
.bind(item.created_at) .bind(item.created_at)
+1 -1
View File
@@ -180,7 +180,7 @@ async fn try_sync_relay(relay: &Relay) -> Result<()> {
"name": relay.info_name, "name": relay.info_name,
"icon": relay.info_icon, "icon": relay.info_icon,
"description": relay.info_description, "description": relay.info_description,
"pubkey": relay.tenant, "pubkey": relay.tenant_pubkey,
}, },
"policy": { "policy": {
"public_join": relay.policy_public_join == 1, "public_join": relay.policy_public_join == 1,
+19 -19
View File
@@ -4,20 +4,6 @@ pub const RELAY_STATUS_ACTIVE: &str = "active";
pub const RELAY_STATUS_INACTIVE: &str = "inactive"; pub const RELAY_STATUS_INACTIVE: &str = "inactive";
pub const RELAY_STATUS_DELINQUENT: &str = "delinquent"; pub const RELAY_STATUS_DELINQUENT: &str = "delinquent";
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct Activity {
pub id: String,
pub tenant: String,
pub created_at: i64,
pub activity_type: String,
pub resource_type: String,
pub resource_id: String,
pub billed_at: Option<i64>,
/// The relay's plan at the time of a `create_relay`/`update_relay` activity;
/// `None` for all other activity types.
pub plan_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Plan { pub struct Plan {
pub id: String, pub id: String,
@@ -41,12 +27,26 @@ pub struct Tenant {
pub renewed_at: Option<i64>, pub renewed_at: Option<i64>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct Activity {
pub id: String,
pub tenant_pubkey: String,
pub created_at: i64,
pub activity_type: String,
pub resource_type: String,
pub resource_id: String,
pub billed_at: Option<i64>,
/// The relay's plan at the time of a `create_relay`/`update_relay` activity;
/// `None` for all other activity types.
pub plan_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)] #[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
pub struct Relay { pub struct Relay {
pub id: String, pub id: String,
pub tenant: String, pub tenant_pubkey: String,
pub subdomain: String, pub subdomain: String,
pub plan: String, pub plan_id: String,
pub status: String, pub status: String,
pub sync_error: String, pub sync_error: String,
pub info_name: String, pub info_name: String,
@@ -66,9 +66,9 @@ impl Default for Relay {
fn default() -> Self { fn default() -> Self {
Self { Self {
id: String::new(), id: String::new(),
tenant: String::new(), tenant_pubkey: String::new(),
subdomain: String::new(), subdomain: String::new(),
plan: String::new(), plan_id: String::new(),
status: RELAY_STATUS_ACTIVE.to_string(), status: RELAY_STATUS_ACTIVE.to_string(),
sync_error: String::new(), sync_error: String::new(),
info_name: String::new(), info_name: String::new(),
@@ -106,7 +106,7 @@ pub struct InvoiceItem {
pub activity_id: Option<String>, pub activity_id: Option<String>,
pub tenant_pubkey: String, pub tenant_pubkey: String,
pub relay_id: String, pub relay_id: String,
pub plan: String, pub plan_id: String,
pub amount: i64, pub amount: i64,
pub description: String, pub description: String,
pub created_at: i64, pub created_at: i64,
+5 -5
View File
@@ -81,9 +81,9 @@ pub async fn list_relays_pending_sync() -> Result<Vec<Relay>> {
) )
} }
pub async fn list_relays_for_tenant(tenant_id: &str) -> 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 = ?")) Ok(sqlx::query_as::<_, Relay>(&select_relay("WHERE tenant_pubkey = ?"))
.bind(tenant_id) .bind(tenant_pubkey)
.fetch_all(pool()) .fetch_all(pool())
.await?) .await?)
} }
@@ -176,7 +176,7 @@ pub async fn get_bolt11_for_invoice(invoice_id: &str) -> Result<Option<Bolt11>>
/// Ordered oldest-first so line items and proration apply in event order. /// Ordered oldest-first so line items and proration apply in event order.
pub async fn list_billable_activity_for_tenant(tenant_pubkey: &str) -> Result<Vec<Activity>> { pub async fn list_billable_activity_for_tenant(tenant_pubkey: &str) -> Result<Vec<Activity>> {
Ok(sqlx::query_as::<_, Activity>(&select_activity( Ok(sqlx::query_as::<_, Activity>(&select_activity(
"WHERE tenant = ? "WHERE tenant_pubkey = ?
AND billed_at IS NULL AND billed_at IS NULL
AND activity_type IN ( AND activity_type IN (
'create_relay', 'update_relay', 'activate_relay', 'deactivate_relay' 'create_relay', 'update_relay', 'activate_relay', 'deactivate_relay'
@@ -197,7 +197,7 @@ pub async fn list_relay_activity_before(
before: i64, before: i64,
) -> Result<Vec<Activity>> { ) -> Result<Vec<Activity>> {
Ok(sqlx::query_as::<_, Activity>(&select_activity( Ok(sqlx::query_as::<_, Activity>(&select_activity(
"WHERE tenant = ? "WHERE tenant_pubkey = ?
AND resource_type = 'relay' AND resource_type = 'relay'
AND activity_type IN ( AND activity_type IN (
'create_relay', 'update_relay', 'activate_relay', 'deactivate_relay' 'create_relay', 'update_relay', 'activate_relay', 'deactivate_relay'
+17 -17
View File
@@ -34,7 +34,7 @@ pub async fn get_relay(
Path(id): Path<String>, Path(id): Path<String>,
) -> ApiResult { ) -> ApiResult {
let relay = api.get_relay_or_404(&id).await?; let relay = api.get_relay_or_404(&id).await?;
api.require_admin_or_tenant(&auth, &relay.tenant)?; api.require_admin_or_tenant(&auth, &relay.tenant_pubkey)?;
ok(relay) ok(relay)
} }
@@ -44,7 +44,7 @@ pub async fn list_relay_activity(
Path(id): Path<String>, Path(id): Path<String>,
) -> ApiResult { ) -> ApiResult {
let relay = api.get_relay_or_404(&id).await?; let relay = api.get_relay_or_404(&id).await?;
api.require_admin_or_tenant(&auth, &relay.tenant)?; api.require_admin_or_tenant(&auth, &relay.tenant_pubkey)?;
let activity = query::list_activity_for_resource(&id) let activity = query::list_activity_for_resource(&id)
.await .await
@@ -58,7 +58,7 @@ pub async fn list_relay_members(
Path(id): Path<String>, Path(id): Path<String>,
) -> ApiResult { ) -> ApiResult {
let relay = api.get_relay_or_404(&id).await?; let relay = api.get_relay_or_404(&id).await?;
api.require_admin_or_tenant(&auth, &relay.tenant)?; api.require_admin_or_tenant(&auth, &relay.tenant_pubkey)?;
let members = fetch_relay_members(&relay).await.map_err(internal)?; let members = fetch_relay_members(&relay).await.map_err(internal)?;
ok(serde_json::json!({ "members": members })) ok(serde_json::json!({ "members": members }))
@@ -66,9 +66,9 @@ pub async fn list_relay_members(
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct CreateRelayRequest { pub struct CreateRelayRequest {
pub tenant: String, pub tenant_pubkey: String,
pub subdomain: String, pub subdomain: String,
pub plan: String, pub plan_id: String,
pub info_name: String, pub info_name: String,
pub info_icon: String, pub info_icon: String,
pub info_description: String, pub info_description: String,
@@ -86,7 +86,7 @@ pub async fn create_relay(
AuthedPubkey(auth): AuthedPubkey, AuthedPubkey(auth): AuthedPubkey,
Json(payload): Json<CreateRelayRequest>, Json(payload): Json<CreateRelayRequest>,
) -> ApiResult { ) -> ApiResult {
api.require_admin_or_tenant(&auth, &payload.tenant)?; api.require_admin_or_tenant(&auth, &payload.tenant_pubkey)?;
let relay_id = format!( let relay_id = format!(
"{}_{}", "{}_{}",
@@ -96,9 +96,9 @@ pub async fn create_relay(
let relay = Relay { let relay = Relay {
id: relay_id.clone(), id: relay_id.clone(),
tenant: payload.tenant, tenant_pubkey: payload.tenant_pubkey,
subdomain: payload.subdomain, subdomain: payload.subdomain,
plan: payload.plan, plan_id: payload.plan_id,
info_name: payload.info_name, info_name: payload.info_name,
info_icon: payload.info_icon, info_icon: payload.info_icon,
info_description: payload.info_description, info_description: payload.info_description,
@@ -124,7 +124,7 @@ pub async fn create_relay(
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct UpdateRelayRequest { pub struct UpdateRelayRequest {
pub subdomain: Option<String>, pub subdomain: Option<String>,
pub plan: Option<String>, pub plan_id: Option<String>,
pub info_name: Option<String>, pub info_name: Option<String>,
pub info_icon: Option<String>, pub info_icon: Option<String>,
pub info_description: Option<String>, pub info_description: Option<String>,
@@ -145,16 +145,16 @@ pub async fn update_relay(
) -> ApiResult { ) -> ApiResult {
let mut relay = api.get_relay_or_404(&id).await?; let mut relay = api.get_relay_or_404(&id).await?;
api.require_admin_or_tenant(&auth, &relay.tenant)?; api.require_admin_or_tenant(&auth, &relay.tenant_pubkey)?;
let current_plan = relay.plan.clone(); let current_plan = relay.plan_id.clone();
let requested_plan = payload.plan.clone(); let requested_plan = payload.plan_id.clone();
if let Some(v) = payload.subdomain { if let Some(v) = payload.subdomain {
relay.subdomain = v; relay.subdomain = v;
} }
if let Some(v) = requested_plan.clone() { if let Some(v) = requested_plan.clone() {
relay.plan = v; relay.plan_id = v;
} }
if let Some(v) = payload.info_name { if let Some(v) = payload.info_name {
relay.info_name = v; relay.info_name = v;
@@ -195,7 +195,7 @@ pub async fn update_relay(
if plan_changed { if plan_changed {
let selected_plan = let selected_plan =
query::get_plan(&relay.plan).expect("validated plan must exist"); query::get_plan(&relay.plan_id).expect("validated plan must exist");
if let Some(limit) = selected_plan.members { if let Some(limit) = selected_plan.members {
let current_members = fetch_relay_members(&relay) let current_members = fetch_relay_members(&relay)
.await .await
@@ -225,7 +225,7 @@ pub async fn deactivate_relay(
Path(id): Path<String>, Path(id): Path<String>,
) -> ApiResult { ) -> ApiResult {
let relay = api.get_relay_or_404(&id).await?; let relay = api.get_relay_or_404(&id).await?;
api.require_admin_or_tenant(&auth, &relay.tenant)?; api.require_admin_or_tenant(&auth, &relay.tenant_pubkey)?;
if relay.status == RELAY_STATUS_DELINQUENT { if relay.status == RELAY_STATUS_DELINQUENT {
return Err(bad_request("relay-is-delinquent", "relay is delinquent")); return Err(bad_request("relay-is-delinquent", "relay is delinquent"));
@@ -248,7 +248,7 @@ pub async fn reactivate_relay(
Path(id): Path<String>, Path(id): Path<String>,
) -> ApiResult { ) -> ApiResult {
let relay = api.get_relay_or_404(&id).await?; let relay = api.get_relay_or_404(&id).await?;
api.require_admin_or_tenant(&auth, &relay.tenant)?; api.require_admin_or_tenant(&auth, &relay.tenant_pubkey)?;
if relay.status == RELAY_STATUS_DELINQUENT { if relay.status == RELAY_STATUS_DELINQUENT {
return Err(bad_request("relay-is-delinquent", "relay is delinquent")); return Err(bad_request("relay-is-delinquent", "relay is delinquent"));
@@ -287,7 +287,7 @@ fn prepare_relay(mut relay: Relay) -> Result<Relay, ApiError> {
return Err(unprocessable("invalid-subdomain", "subdomain is invalid")); return Err(unprocessable("invalid-subdomain", "subdomain is invalid"));
} }
let plan = query::get_plan(&relay.plan) let plan = query::get_plan(&relay.plan_id)
.ok_or_else(|| unprocessable("invalid-plan", "plan not found"))?; .ok_or_else(|| unprocessable("invalid-plan", "plan not found"))?;
if (!plan.blossom && relay.blossom_enabled == 1) || (!plan.livekit && relay.livekit_enabled == 1) { if (!plan.blossom && relay.blossom_enabled == 1) || (!plan.livekit && relay.livekit_enabled == 1) {
+2 -2
View File
@@ -32,7 +32,7 @@ export default function PaymentDialog(props: PaymentDialogProps) {
const billedRelays = createMemo(() => { const billedRelays = createMemo(() => {
const planById = new Map(plans().map((p) => [p.id, p])) const planById = new Map(plans().map((p) => [p.id, p]))
return (relays() ?? []) return (relays() ?? [])
.map((relay) => ({ relay, plan: planById.get(relay.plan) })) .map((relay) => ({ relay, plan: planById.get(relay.plan_id) }))
.filter((entry) => Boolean(entry.plan?.amount)) .filter((entry) => Boolean(entry.plan?.amount))
}) })
@@ -147,7 +147,7 @@ export default function PaymentDialog(props: PaymentDialogProps) {
<li class="flex items-center justify-between gap-3 text-sm"> <li class="flex items-center justify-between gap-3 text-sm">
<span class="truncate text-gray-900">{relay.info_name || relay.subdomain}</span> <span class="truncate text-gray-900">{relay.info_name || relay.subdomain}</span>
<span class="flex-shrink-0 text-xs text-gray-500"> <span class="flex-shrink-0 text-xs text-gray-500">
{plan?.name ?? relay.plan} {plan?.name ?? relay.plan_id}
<Show when={plan}> · ${(plan!.amount / 100).toFixed(2)}/mo</Show> <Show when={plan}> · ${(plan!.amount / 100).toFixed(2)}/mo</Show>
</span> </span>
</li> </li>
+4 -4
View File
@@ -32,9 +32,9 @@ function memberLabel(members: number | null) {
type PricingTableProps = { type PricingTableProps = {
selectable?: boolean selectable?: boolean
selectedPlan?: PlanId selectedPlanId?: PlanId
onSelect?: (plan: PlanId) => void onSelect?: (planId: PlanId) => void
onCta?: (plan: PlanId) => void onCta?: (planId: PlanId) => void
} }
export default function PricingTable(props: PricingTableProps) { export default function PricingTable(props: PricingTableProps) {
@@ -43,7 +43,7 @@ export default function PricingTable(props: PricingTableProps) {
<For each={plans()}> <For each={plans()}>
{(plan) => { {(plan) => {
const isPopular = plan.id === "basic" const isPopular = plan.id === "basic"
const isSelected = () => props.selectable && props.selectedPlan === plan.id const isSelected = () => props.selectable && props.selectedPlanId === plan.id
const card = ( const card = (
<> <>
+12 -12
View File
@@ -63,7 +63,7 @@ type RelayDetailCardProps = {
onToggleMediaStorage?: () => void onToggleMediaStorage?: () => void
onToggleLivekitSupport?: () => void onToggleLivekitSupport?: () => void
onTogglePushNotifications?: () => void onTogglePushNotifications?: () => void
onUpdatePlan?: (plan: PlanId) => Promise<void> onUpdatePlan?: (planId: PlanId) => Promise<void>
enforcePlanLimits?: boolean enforcePlanLimits?: boolean
showPlanActions?: boolean showPlanActions?: boolean
} }
@@ -76,17 +76,17 @@ export default function RelayDetailCard(props: RelayDetailCardProps) {
return fallback return fallback
} }
const [menuOpen, setMenuOpen] = createSignal(false) const [menuOpen, setMenuOpen] = createSignal(false)
const [plan, setPlan] = createSignal<PlanId>(props.relay.plan) const [planId, setPlanId] = createSignal<PlanId>(props.relay.plan_id)
const [pendingAction, setPendingAction] = createSignal<"deactivate" | "reactivate" | null>(null) const [pendingAction, setPendingAction] = createSignal<"deactivate" | "reactivate" | null>(null)
let menuContainerRef: HTMLDivElement | undefined let menuContainerRef: HTMLDivElement | undefined
const memberLimitLabel = () => { const memberLimitLabel = () => {
const p = plans().find(p => p.id === r().plan) const p = plans().find(p => p.id === r().plan_id)
if (!p) return "?" if (!p) return "?"
return p.members === null ? "∞" : String(p.members) return p.members === null ? "∞" : String(p.members)
} }
const planLimited = () => (props.enforcePlanLimits ?? true) && r().plan === "free" const planLimited = () => (props.enforcePlanLimits ?? true) && r().plan_id === "free"
const showPlanActions = () => props.showPlanActions ?? true const showPlanActions = () => props.showPlanActions ?? true
const actionBusy = () => pendingAction() === "deactivate" ? !!props.deactivating : pendingAction() === "reactivate" ? !!props.reactivating : false const actionBusy = () => pendingAction() === "deactivate" ? !!props.deactivating : pendingAction() === "reactivate" ? !!props.reactivating : false
const relayLabel = () => r().info_name || r().subdomain const relayLabel = () => r().info_name || r().subdomain
@@ -107,11 +107,11 @@ export default function RelayDetailCard(props: RelayDetailCardProps) {
const confirmBusyLabel = () => pendingAction() === "deactivate" ? "Deactivating..." : "Reactivating..." const confirmBusyLabel = () => pendingAction() === "deactivate" ? "Deactivating..." : "Reactivating..."
const confirmTone = () => pendingAction() === "deactivate" ? "danger" : "primary" const confirmTone = () => pendingAction() === "deactivate" ? "danger" : "primary"
async function changePlan(plan: PlanId) { async function changePlanId(planId: PlanId) {
setPlan(plan) setPlanId(planId)
try { try {
await props.onUpdatePlan?.(plan) await props.onUpdatePlan?.(planId)
setToastMessage(`Plan updated to ${plan}`, "success") setToastMessage(`Plan updated to ${planId}`, "success")
} catch { } catch {
// error is handled by the caller // error is handled by the caller
} }
@@ -360,7 +360,7 @@ export default function RelayDetailCard(props: RelayDetailCardProps) {
</Field> </Field>
<Show when={props.showTenant}> <Show when={props.showTenant}>
<Field label="Tenant"> <Field label="Tenant">
<span class="font-mono text-xs break-all">{r().tenant}</span> <span class="font-mono text-xs break-all">{r().tenant_pubkey}</span>
</Field> </Field>
</Show> </Show>
</MembershipSection> </MembershipSection>
@@ -373,15 +373,15 @@ export default function RelayDetailCard(props: RelayDetailCardProps) {
when={props.onUpdatePlan} when={props.onUpdatePlan}
fallback={ fallback={
<Field label="Current plan"> <Field label="Current plan">
<span class="capitalize text-gray-900">{r().plan}</span> <span class="capitalize text-gray-900">{r().plan_id}</span>
</Field> </Field>
} }
> >
<div class="lg:col-span-2 space-y-4"> <div class="lg:col-span-2 space-y-4">
<PricingTable <PricingTable
selectable selectable
selectedPlan={plan()} selectedPlanId={planId()}
onSelect={changePlan} onSelect={changePlanId}
/> />
</div> </div>
</Show> </Show>
+8 -8
View File
@@ -5,7 +5,7 @@ import { validateSubdomainLabel } from "@/lib/subdomain"
import { setToastMessage } from "@/components/Toast" import { setToastMessage } from "@/components/Toast"
import { plans } from "@/lib/state" import { plans } from "@/lib/state"
export type RelayFormValues = Pick<Relay, "info_name" | "subdomain" | "info_icon" | "info_description" | "plan"> export type RelayFormValues = Pick<Relay, "info_name" | "subdomain" | "info_icon" | "info_description" | "plan_id">
type RelayFormProps = { type RelayFormProps = {
initialValues?: Partial<RelayFormValues> initialValues?: Partial<RelayFormValues>
@@ -16,8 +16,8 @@ type RelayFormProps = {
} }
export default function RelayForm(props: RelayFormProps) { export default function RelayForm(props: RelayFormProps) {
const defaultPlanId = createMemo(() => props.initialValues?.plan ?? plans()[0]?.id ?? "free") const defaultPlanId = createMemo(() => props.initialValues?.plan_id ?? plans()[0]?.id ?? "free")
const [plan, setPlan] = createSignal(defaultPlanId()) const [planId, setPlanId] = createSignal(defaultPlanId())
const [name, setName] = createSignal(props.initialValues?.info_name ?? "") const [name, setName] = createSignal(props.initialValues?.info_name ?? "")
const [subdomain, setSubdomain] = createSignal(props.initialValues?.subdomain ?? "") const [subdomain, setSubdomain] = createSignal(props.initialValues?.subdomain ?? "")
const [icon, setIcon] = createSignal(props.initialValues?.info_icon ?? "") const [icon, setIcon] = createSignal(props.initialValues?.info_icon ?? "")
@@ -27,7 +27,7 @@ export default function RelayForm(props: RelayFormProps) {
async function handleSubmit(e: Event) { async function handleSubmit(e: Event) {
e.preventDefault() e.preventDefault()
if (!plan()) { if (!planId()) {
setToastMessage("Please select a plan") setToastMessage("Please select a plan")
return return
} }
@@ -43,7 +43,7 @@ export default function RelayForm(props: RelayFormProps) {
try { try {
await props.onSubmit({ await props.onSubmit({
plan: plan(), plan_id: planId(),
info_name: name(), info_name: name(),
subdomain: subdomain(), subdomain: subdomain(),
info_icon: icon(), info_icon: icon(),
@@ -56,7 +56,7 @@ export default function RelayForm(props: RelayFormProps) {
} }
} }
createEffect(() => setPlan(defaultPlanId())) createEffect(() => setPlanId(defaultPlanId()))
createEffect(() => { createEffect(() => {
if (props.syncSubdomainWithName) { if (props.syncSubdomainWithName) {
@@ -112,8 +112,8 @@ export default function RelayForm(props: RelayFormProps) {
{(p) => ( {(p) => (
<button <button
type="button" type="button"
onClick={() => setPlan(p.id)} onClick={() => setPlanId(p.id)}
class={`border-2 rounded-xl p-4 text-left transition-colors ${plan() === p.id ? "border-blue-600 bg-blue-50" : "border-gray-200 hover:border-gray-300"}`} class={`border-2 rounded-xl p-4 text-left transition-colors ${planId() === p.id ? "border-blue-600 bg-blue-50" : "border-gray-200 hover:border-gray-300"}`}
> >
<div class="font-bold text-gray-900">{p.name}</div> <div class="font-bold text-gray-900">{p.name}</div>
<div class="text-sm text-gray-500 mt-1"> <div class="text-sm text-gray-500 mt-1">
+1 -1
View File
@@ -17,7 +17,7 @@ export default function RelayListItem(props: RelayListItemProps) {
<p class="font-medium text-gray-900">{props.relay.info_name || props.relay.subdomain}</p> <p class="font-medium text-gray-900">{props.relay.info_name || props.relay.subdomain}</p>
<p class="text-xs text-gray-500">{props.relay.subdomain}.spaces.coracle.social</p> <p class="text-xs text-gray-500">{props.relay.subdomain}.spaces.coracle.social</p>
{props.showTenant && ( {props.showTenant && (
<p class="text-xs text-gray-500 break-all mt-1">Tenant: {props.relay.tenant}</p> <p class="text-xs text-gray-500 break-all mt-1">Tenant: {props.relay.tenant_pubkey}</p>
)} )}
</div> </div>
<Show <Show
+6 -6
View File
@@ -44,9 +44,9 @@ export type PlanId = string
export type Relay = { export type Relay = {
id: string id: string
tenant: string tenant_pubkey: string
subdomain: string subdomain: string
plan: PlanId plan_id: PlanId
status: string status: string
sync_error: string sync_error: string
synced: number synced: number
@@ -63,9 +63,9 @@ export type Relay = {
} }
export type CreateRelayInput = { export type CreateRelayInput = {
tenant?: string tenant_pubkey?: string
subdomain: string subdomain: string
plan: string plan_id: string
info_name?: string info_name?: string
info_icon?: string info_icon?: string
info_description?: string info_description?: string
@@ -80,7 +80,7 @@ export type CreateRelayInput = {
export type UpdateRelayInput = { export type UpdateRelayInput = {
subdomain?: string subdomain?: string
plan?: string plan_id?: string
info_name?: string info_name?: string
info_icon?: string info_icon?: string
info_description?: string info_description?: string
@@ -115,7 +115,7 @@ export type Invoice = {
export type Activity = { export type Activity = {
id: string id: string
tenant: string tenant_pubkey: string
created_at: number created_at: number
activity_type: string activity_type: string
resource_type: string resource_type: string
+3 -3
View File
@@ -116,8 +116,8 @@ export const createRelayForActiveTenant = (input: CreateRelayInput) => {
const overrides = { const overrides = {
tenant: account()!.pubkey, tenant: account()!.pubkey,
blossom_enabled: input.plan === "free" ? 0 : 1, blossom_enabled: input.plan_id === "free" ? 0 : 1,
livekit_enabled: input.plan === "free" ? 0 : 1, livekit_enabled: input.plan_id === "free" ? 0 : 1,
} }
return createRelay({...defaults, ...input, ...overrides}) return createRelay({...defaults, ...input, ...overrides})
@@ -127,7 +127,7 @@ export const updateActiveTenant = (input: { nwc_url?: string }) => updateTenant(
export const updateRelayById = (id: string, input: UpdateRelayInput) => updateRelay(id, input) export const updateRelayById = (id: string, input: UpdateRelayInput) => updateRelay(id, input)
export const updateRelayPlanById = (id: string, plan: string) => updateRelay(id, { plan }) export const updateRelayPlanById = (id: string, plan_id: string) => updateRelay(id, { plan_id })
export const deactivateRelayById = (id: string) => deactivateRelay(id) export const deactivateRelayById = (id: string) => deactivateRelay(id)
+7 -7
View File
@@ -77,14 +77,14 @@ export default function useRelayToggles(
} }
} }
async function handleUpdatePlan(plan: PlanId) { async function handleUpdatePlan(plan_id: PlanId) {
const current = relay() const current = relay()
if (!current) return if (!current) return
const previous = current const previous = current
const next = { ...current, plan } const next = { ...current, plan_id }
const update: Record<string, unknown> = { plan } const update: Record<string, unknown> = { plan_id }
if (plan === "free") { if (plan_id === "free") {
next.blossom_enabled = 0 next.blossom_enabled = 0
next.livekit_enabled = 0 next.livekit_enabled = 0
update.blossom_enabled = 0 update.blossom_enabled = 0
@@ -101,7 +101,7 @@ export default function useRelayToggles(
throw e throw e
} }
if (plan !== "free") { if (plan_id !== "free") {
const needsSetup = await tenantNeedsPaymentSetup() const needsSetup = await tenantNeedsPaymentSetup()
if (needsSetup) { if (needsSetup) {
const invoice = await getLatestOpenInvoice() const invoice = await getLatestOpenInvoice()
@@ -116,9 +116,9 @@ export default function useRelayToggles(
onToggleStripSignatures: () => toggle("policy_strip_signatures", false), onToggleStripSignatures: () => toggle("policy_strip_signatures", false),
onToggleGroups: () => toggle("groups_enabled", true), onToggleGroups: () => toggle("groups_enabled", true),
onToggleManagement: () => toggle("management_enabled", true), onToggleManagement: () => toggle("management_enabled", true),
onToggleMediaStorage: () => toggle("blossom_enabled", relay()?.plan !== "free"), onToggleMediaStorage: () => toggle("blossom_enabled", relay()?.plan_id !== "free"),
onTogglePushNotifications: () => toggle("push_enabled", true), onTogglePushNotifications: () => toggle("push_enabled", true),
onToggleLivekitSupport: () => toggle("livekit_enabled", relay()?.plan !== "free"), onToggleLivekitSupport: () => toggle("livekit_enabled", relay()?.plan_id !== "free"),
} }
return { busy, handleDeactivate, handleReactivate, handleUpdatePlan, pendingInvoice, clearPendingInvoice: () => setPendingInvoice(undefined), pendingPaymentSetup, clearPendingPaymentSetup: () => setPendingPaymentSetup(false), toggles } return { busy, handleDeactivate, handleReactivate, handleUpdatePlan, pendingInvoice, clearPendingInvoice: () => setPendingInvoice(undefined), pendingPaymentSetup, clearPendingPaymentSetup: () => setPendingPaymentSetup(false), toggles }
+4 -4
View File
@@ -17,10 +17,10 @@ export default function Home() {
const [showRelayModal, setShowRelayModal] = createSignal(false) const [showRelayModal, setShowRelayModal] = createSignal(false)
const [showLoginModal, setShowLoginModal] = createSignal(false) const [showLoginModal, setShowLoginModal] = createSignal(false)
const [draftRelay, setDraftRelay] = createSignal<RelayFormValues>() const [draftRelay, setDraftRelay] = createSignal<RelayFormValues>()
const [initialPlan, setInitialPlan] = createSignal<RelayFormValues["plan"]>("free") const [initialPlanId, setInitialPlanId] = createSignal<RelayFormValues["plan_id"]>("free")
function openRelayModal(plan: RelayFormValues["plan"] = "free") { function openRelayModal(planId: RelayFormValues["plan_id"] = "free") {
setInitialPlan(plan) setInitialPlanId(planId)
setShowRelayModal(true) setShowRelayModal(true)
} }
@@ -404,7 +404,7 @@ export default function Home() {
<RelayForm <RelayForm
syncSubdomainWithName syncSubdomainWithName
initialValues={{ plan: initialPlan() }} initialValues={{ plan_id: initialPlanId() }}
onSubmit={onRelayFormSubmit} onSubmit={onRelayFormSubmit}
submitLabel="Continue" submitLabel="Continue"
submittingLabel="Creating..." submittingLabel="Creating..."
+1 -1
View File
@@ -45,7 +45,7 @@ export default function RelayDetail() {
const isPaidRelay = createMemo(() => { const isPaidRelay = createMemo(() => {
const r = relay() const r = relay()
if (!r) return false if (!r) return false
const plan = plans().find(p => p.id === r.plan) const plan = plans().find(p => p.id === r.plan_id)
return !!(plan && plan.amount > 0) return !!(plan && plan.amount > 0)
}) })
+1 -1
View File
@@ -18,7 +18,7 @@ export default function RelayNew() {
const relay = await createRelayForActiveTenant(values) const relay = await createRelayForActiveTenant(values)
createdRelayId = relay.id createdRelayId = relay.id
if (values.plan !== "free") { if (values.plan_id !== "free") {
const needsSetup = await tenantNeedsPaymentSetup() const needsSetup = await tenantNeedsPaymentSetup()
if (needsSetup) { if (needsSetup) {
const invoice = await getLatestOpenInvoice() const invoice = await getLatestOpenInvoice()