forked from coracle/caravel
Improve transactionality, align invoice model with frontend
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user