Improve transactionality, align invoice model with frontend

This commit is contained in:
Jon Staab
2026-05-29 13:37:11 -07:00
parent ae3e1c316e
commit 0018a5d4f3
12 changed files with 305 additions and 309 deletions
+2 -2
View File
@@ -2,7 +2,7 @@ import { A, useLocation } from "@solidjs/router"
import { createEffect, createMemo, createSignal, For, onCleanup, Show } from "solid-js"
import Fuse from "fuse.js"
import { primeProfiles, useProfilePicture, useTenant, useTenantRelays, type Relay } from "@/lib/hooks"
import { listTenantInvoices, type Invoice } from "@/lib/api"
import { invoiceStatus, listTenantInvoices, type Invoice } from "@/lib/api"
import { account, eventStore, identity } from "@/lib/state"
import serverIcon from "@/assets/server.svg"
import Modal from "@/components/Modal"
@@ -51,7 +51,7 @@ export default function AppShell(props: { children?: any }) {
}
try {
const invoices = await listTenantInvoices(t.pubkey)
const openInvoice = invoices.find(inv => inv.status === "open")
const openInvoice = invoices.find(inv => invoiceStatus(inv) === "open")
setPastDueInvoice(openInvoice)
} catch {
// ignore
+3 -3
View File
@@ -9,7 +9,7 @@ import { plans } from "@/lib/state"
type PayStatus = "idle" | "loading" | "success" | "error"
type Bolt11Status = "idle" | "loading" | "ready" | "error"
type PaymentInvoice = Pick<Invoice, "id" | "amount_due"> &
type PaymentInvoice = Pick<Invoice, "id" | "amount"> &
Partial<Pick<Invoice, "period_start" | "period_end">>
type PaymentDialogProps = {
@@ -68,7 +68,7 @@ export default function PaymentDialog(props: PaymentDialogProps) {
setPayError("")
try {
const invoice = await getInvoice(props.invoice.id)
if (invoice.status === "paid") {
if (invoice.paid_at != null) {
setPayStatus("success")
} else {
setPayStatus("error")
@@ -90,7 +90,7 @@ export default function PaymentDialog(props: PaymentDialogProps) {
props.onClose()
}
const amountLabel = () => `$${(props.invoice.amount_due / 100).toFixed(2)}`
const amountLabel = () => `$${(props.invoice.amount / 100).toFixed(2)}`
const periodLabel = () => {
const { period_start, period_end } = props.invoice
+14 -4
View File
@@ -106,12 +106,22 @@ export type Tenant = {
export type Invoice = {
id: string
customer: string
status: string
amount_due: number
currency: string
tenant_pubkey: string
amount: number
period_start: number
period_end: number
created_at: number
paid_at: number | null
voided_at: number | null
}
// The backend models an invoice's lifecycle as timestamps rather than a status
// field, so derive the display status from them: paid once paid_at is set, void
// once voided_at is set, otherwise still open.
export function invoiceStatus(invoice: Pick<Invoice, "paid_at" | "voided_at">): "open" | "paid" | "void" {
if (invoice.paid_at != null) return "paid"
if (invoice.voided_at != null) return "void"
return "open"
}
export type Activity = {
+2 -1
View File
@@ -9,6 +9,7 @@ import {
reactivateRelay,
getRelay,
getTenant,
invoiceStatus,
listRelayActivity,
listRelays,
listTenantInvoices,
@@ -141,7 +142,7 @@ export async function tenantNeedsPaymentSetup(): Promise<boolean> {
export async function getLatestOpenInvoice(): Promise<Invoice | null> {
const invoices = await listTenantInvoices(account()!.pubkey)
const open = invoices
.filter(inv => inv.status === "open" && inv.amount_due > 0)
.filter(inv => invoiceStatus(inv) === "open" && inv.amount > 0)
.sort((a, b) => b.period_start - a.period_start)
return open[0] ?? null
}
+6 -7
View File
@@ -5,7 +5,7 @@ import LoadingState from "@/components/LoadingState"
import PaymentDialog from "@/components/PaymentDialog"
import useMinLoading from "@/components/useMinLoading"
import { updateActiveTenant, useTenant } from "@/lib/hooks"
import { createPortalSession, getInvoice, listTenantInvoices, type Invoice } from "@/lib/api"
import { createPortalSession, getInvoice, invoiceStatus, listTenantInvoices, type Invoice } from "@/lib/api"
import { account } from "@/lib/state"
export default function Account() {
@@ -75,11 +75,9 @@ export default function Account() {
}
const invoiceStatusStyles: Record<string, string> = {
draft: "bg-gray-100 text-gray-500 border-gray-200",
open: "bg-yellow-50 text-yellow-700 border-yellow-200",
paid: "bg-green-50 text-green-700 border-green-200",
void: "bg-gray-100 text-gray-500 border-gray-200",
uncollectible: "bg-red-50 text-red-700 border-red-200",
}
return (
@@ -168,8 +166,9 @@ export default function Account() {
<ul class="space-y-3">
<For each={invoices()}>
{(invoice) => {
const isOpen = () => invoice.status === "open"
const statusStyle = () => invoiceStatusStyles[invoice.status] ?? "bg-gray-100 text-gray-500 border-gray-200"
const status = () => invoiceStatus(invoice)
const isOpen = () => status() === "open"
const statusStyle = () => invoiceStatusStyles[status()] ?? "bg-gray-100 text-gray-500 border-gray-200"
const periodLabel = () => {
const start = new Date(invoice.period_start * 1000)
const end = new Date(invoice.period_end * 1000)
@@ -185,7 +184,7 @@ export default function Account() {
<div class="flex items-center justify-between gap-3">
<div>
<span class="font-medium text-gray-900">
${(invoice.amount_due / 100).toFixed(2)}
${(invoice.amount / 100).toFixed(2)}
</span>
<Show when={invoice.period_start && invoice.period_end}>
<p class="text-xs text-gray-500 mt-0.5">{periodLabel()}</p>
@@ -196,7 +195,7 @@ export default function Account() {
<span class="text-xs text-blue-600 font-medium">Pay now</span>
</Show>
<span class={`inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium capitalize ${statusStyle()}`}>
{invoice.status}
{status()}
</span>
</div>
</div>