Add dunning

This commit is contained in:
Jon Staab
2026-05-29 11:32:06 -07:00
parent f7bd3e53fe
commit d5047dedb1
13 changed files with 331 additions and 74 deletions
+3 -3
View File
@@ -45,7 +45,7 @@ export default function AppShell(props: { children?: any }) {
createEffect(async () => {
const t = tenant()
if (!t?.past_due_at) {
if (!t?.churned_at) {
setPastDueInvoice(undefined)
return
}
@@ -158,9 +158,9 @@ export default function AppShell(props: { children?: any }) {
</aside>
<div class="md:pl-[260px] min-h-screen pb-20 md:pb-0">
<Show when={tenant()?.past_due_at}>
<Show when={tenant()?.churned_at}>
<div class="bg-red-600 text-white px-4 py-3 text-sm flex items-center justify-between">
<span>Your account has an overdue balance.</span>
<span>Your account is past due and some relays are paused. Update your payment method to restore service.</span>
<Show when={pastDueInvoice()}>
<button
type="button"
+2 -4
View File
@@ -98,9 +98,9 @@ export type Tenant = {
nwc_is_set: boolean
created_at: number
stripe_customer_id: string
stripe_subscription_id: string | null
past_due_at: number | null
nwc_error: string | null
stripe_error: string | null
churned_at: number | null
}
export type Invoice = {
@@ -142,8 +142,6 @@ export async function makeAuth(): Promise<string | undefined> {
kind: 27235,
content: "",
created_at: Math.floor(now / 1000),
// Intentional session-style auth: sign the API base URL once, then reuse
// the header briefly to avoid prompting the signer on every request.
tags: [["u", API_URL]],
})
+1 -1
View File
@@ -135,7 +135,7 @@ export const reactivateRelayById = (id: string) => reactivateRelay(id)
export async function tenantNeedsPaymentSetup(): Promise<boolean> {
const tenant = await getTenant(account()!.pubkey)
return !tenant.nwc_is_set && !tenant.stripe_subscription_id
return !tenant.nwc_is_set
}
export async function getLatestOpenInvoice(): Promise<Invoice | null> {
+9 -1
View File
@@ -142,8 +142,16 @@ export default function Account() {
{saving() ? "Saving..." : "Save"}
</button>
</div>
<Show when={tenant()?.churned_at}>
<p class="mb-4 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700">
Your account is past due and some relays have been paused. Update your payment method below to restore service.
</p>
</Show>
<Show when={tenant()?.nwc_error}>
<p class="mt-3 text-sm text-red-600">{tenant()!.nwc_error}</p>
<p class="mt-3 text-sm text-red-600">Lightning auto-payment failed: {tenant()!.nwc_error}</p>
</Show>
<Show when={tenant()?.stripe_error}>
<p class="mt-3 text-sm text-red-600">Card auto-payment failed: {tenant()!.stripe_error}</p>
</Show>
<Show when={error()}>
<p class="mt-3 text-sm text-red-600">{error()}</p>
+12 -6
View File
@@ -14,8 +14,8 @@ export default function AdminTenantDetail() {
const [relays] = useAdminTenantRelays(tenantId)
const loading = useMinLoading(() => tenant.loading || relays.loading)
const pastDueLabel = () => {
const ts = tenant()?.past_due_at
const churnedLabel = () => {
const ts = tenant()?.churned_at
if (!ts) return null
return new Date(ts * 1000).toLocaleString()
}
@@ -34,7 +34,7 @@ export default function AdminTenantDetail() {
<dl class="grid gap-y-3 text-sm">
<div class="flex gap-2">
<dt class="text-gray-500">Status:</dt>
<dd class="font-medium uppercase tracking-wide">{t().past_due_at ? "past due" : "active"}</dd>
<dd class="font-medium uppercase tracking-wide">{t().churned_at ? "delinquent" : "active"}</dd>
</div>
<Show when={t().stripe_customer_id}>
<div class="flex gap-2">
@@ -42,10 +42,10 @@ export default function AdminTenantDetail() {
<dd class="font-mono text-xs">{t().stripe_customer_id}</dd>
</div>
</Show>
<Show when={pastDueLabel()}>
<Show when={churnedLabel()}>
<div class="flex gap-2">
<dt class="text-gray-500">Past Due Since:</dt>
<dd class="text-red-600 font-medium">{pastDueLabel()}</dd>
<dt class="text-gray-500">Delinquent Since:</dt>
<dd class="text-red-600 font-medium">{churnedLabel()}</dd>
</div>
</Show>
<Show when={t().nwc_error}>
@@ -54,6 +54,12 @@ export default function AdminTenantDetail() {
<dd class="text-red-600">{t().nwc_error}</dd>
</div>
</Show>
<Show when={t().stripe_error}>
<div class="flex gap-2">
<dt class="text-gray-500">Stripe Error:</dt>
<dd class="text-red-600">{t().stripe_error}</dd>
</div>
</Show>
</dl>
)}
</Show>