Allow Lightning invoice reuse and re-payment from the dashboard.

Store LNbits invoice data on payments, add an invoice endpoint that reuses valid invoices or regenerates expired ones, and wire the booking payment page to fetch and display invoices via a shared watcher hook.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Michilis
2026-07-01 16:48:28 +00:00
co-authored by Cursor
parent 09bfb17f03
commit 78ffd0ae52
10 changed files with 331 additions and 34 deletions
+18
View File
@@ -238,6 +238,15 @@ async function migrate() {
try {
await (db as any).run(sql`ALTER TABLE payments ADD COLUMN reminder_sent_at TEXT`);
} catch (e) { /* column may already exist */ }
try {
await (db as any).run(sql`ALTER TABLE payments ADD COLUMN lnbits_invoice TEXT`);
} catch (e) { /* column may already exist */ }
try {
await (db as any).run(sql`ALTER TABLE payments ADD COLUMN lnbits_expires_at TEXT`);
} catch (e) { /* column may already exist */ }
try {
await (db as any).run(sql`ALTER TABLE payments ADD COLUMN lnbits_amount_sats INTEGER`);
} catch (e) { /* column may already exist */ }
// Invoices table
await (db as any).run(sql`
@@ -719,6 +728,15 @@ async function migrate() {
try {
await (db as any).execute(sql`ALTER TABLE payments ADD COLUMN reminder_sent_at TIMESTAMP`);
} catch (e) { /* column may already exist */ }
try {
await (db as any).execute(sql`ALTER TABLE payments ADD COLUMN lnbits_invoice TEXT`);
} catch (e) { /* column may already exist */ }
try {
await (db as any).execute(sql`ALTER TABLE payments ADD COLUMN lnbits_expires_at TIMESTAMP`);
} catch (e) { /* column may already exist */ }
try {
await (db as any).execute(sql`ALTER TABLE payments ADD COLUMN lnbits_amount_sats INTEGER`);
} catch (e) { /* column may already exist */ }
// Invoices table
await (db as any).execute(sql`
+6
View File
@@ -121,6 +121,9 @@ export const sqlitePayments = sqliteTable('payments', {
currency: text('currency').notNull().default('PYG'),
status: text('status', { enum: ['pending', 'pending_approval', 'paid', 'refunded', 'failed', 'cancelled', 'on_hold'] }).notNull().default('pending'),
reference: text('reference'),
lnbitsInvoice: text('lnbits_invoice'), // BOLT11 string, stored so an unpaid invoice can be redisplayed without a new LNbits call
lnbitsExpiresAt: text('lnbits_expires_at'), // When lnbitsInvoice expires
lnbitsAmountSats: integer('lnbits_amount_sats'), // Sats amount for display (from LNbits' fiat conversion)
userMarkedPaidAt: text('user_marked_paid_at'), // When user clicked "I Have Paid"
payerName: text('payer_name'), // Name of payer if different from attendee
paidAt: text('paid_at'),
@@ -476,6 +479,9 @@ export const pgPayments = pgTable('payments', {
currency: varchar('currency', { length: 10 }).notNull().default('PYG'),
status: varchar('status', { length: 20 }).notNull().default('pending'),
reference: varchar('reference', { length: 255 }),
lnbitsInvoice: pgText('lnbits_invoice'), // BOLT11 string, stored so an unpaid invoice can be redisplayed without a new LNbits call
lnbitsExpiresAt: timestamp('lnbits_expires_at'), // When lnbitsInvoice expires
lnbitsAmountSats: pgInteger('lnbits_amount_sats'), // Sats amount for display (from LNbits' fiat conversion)
userMarkedPaidAt: timestamp('user_marked_paid_at'),
payerName: varchar('payer_name', { length: 255 }), // Name of payer if different from attendee
paidAt: timestamp('paid_at'),