Restructure reconciliation to always reconcile oob payments

This commit is contained in:
Jon Staab
2026-06-03 10:19:52 -07:00
parent b702733559
commit 0e18d4020a
5 changed files with 76 additions and 74 deletions
+6 -28
View File
@@ -7,9 +7,9 @@ import { useInvoicePdf } from "@/lib/useInvoicePdf"
import PaymentSetupNWC from "@/components/PaymentSetupNWC"
import PaymentSetupCard from "@/components/PaymentSetupCard"
import { useBillingStatus, accountStatus, type AccountStatus } from "@/lib/billing"
import { invoiceStatus, listDraftInvoiceItems, reconcileInvoice, type Invoice } from "@/lib/api"
import { invoiceStatus, listDraftInvoiceItems, type Invoice } from "@/lib/api"
import { cardState, methodLabel, nwcState, type PaymentMethodState } from "@/lib/paymentMethod"
import { account, setToastMessage } from "@/lib/state"
import { account } from "@/lib/state"
import { formatPeriod } from "@/lib/format"
import PaymentMethodRow from "@/components/account/PaymentMethodRow"
import InvoiceListItem from "@/components/account/InvoiceListItem"
@@ -46,37 +46,15 @@ export default function Account() {
// composite: reconcile the subscription, sync a card just added in the portal,
// and collect the open invoice if a method is now on file — then refresh. This
// is what pays the outstanding invoice after the user adds a card and returns.
// Reconciles on landing (including after returning from a Stripe Checkout or
// the billing portal): reconcile_tenant settles any out-of-band payment — a
// completed Checkout or a bolt11 paid elsewhere — and collects when a method
// is on file, then refreshes. No per-invoice return marker needed.
createEffect(() => {
const pubkey = account()?.pubkey
if (pubkey) void billing.autopay(pubkey)
})
// Returning from a per-invoice Stripe Checkout (the success_url carries
// ?paid_invoice=ID): reconcile that invoice so it flips to paid promptly —
// autopay above only collects when a recurring method is on file, and a
// one-off Checkout payment doesn't leave one — then strip the marker.
createEffect(() => {
const pubkey = account()?.pubkey
const paidInvoice = new URLSearchParams(window.location.search).get("paid_invoice")
if (!pubkey || !paidInvoice) return
void (async () => {
try {
const invoice = await reconcileInvoice(paidInvoice)
setToastMessage(
invoice.paid_at != null ? "Payment received. Thank you!" : "Your payment is still processing.",
)
} catch (e) {
setToastMessage(e instanceof Error ? e.message : "Failed to confirm payment")
} finally {
const params = new URLSearchParams(window.location.search)
params.delete("paid_invoice")
const qs = params.toString()
window.history.replaceState({}, "", `${window.location.pathname}${qs ? `?${qs}` : ""}`)
billing.refetch()
}
})()
})
// Coarse account-health summary for the badge. Same snapshot the inline prompt
// consumes, so the two can never disagree.
const status = createMemo(() =>