diff --git a/static/js/index.js b/static/js/index.js index 9a29711..c13c628 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -92,7 +92,11 @@ window.app = Vue.createApp({ payment_method: 'cash', description: '', reference: '', - loading: false + loading: false, + invoice: null, + paymentHash: null, + checkWalletKey: null, + pollIntervalId: null } } }, @@ -103,6 +107,13 @@ window.app = Vue.createApp({ clearInterval(this.payDialog.pollIntervalId) this.payDialog.pollIntervalId = null } + }, + 'settleReceivableDialog.show': function(newVal) { + // When dialog is closed, stop polling + if (!newVal && this.settleReceivableDialog.pollIntervalId) { + clearInterval(this.settleReceivableDialog.pollIntervalId) + this.settleReceivableDialog.pollIntervalId = null + } } }, computed: { @@ -867,18 +878,132 @@ window.app = Vue.createApp({ // Only show for users who owe castle (negative balance) if (userBalance.balance >= 0) return + // Clear any existing polling + if (this.settleReceivableDialog.pollIntervalId) { + clearInterval(this.settleReceivableDialog.pollIntervalId) + } + this.settleReceivableDialog = { show: true, user_id: userBalance.user_id, username: userBalance.username, maxAmount: Math.abs(userBalance.balance), // Convert negative to positive amount: Math.abs(userBalance.balance), // Default to full amount - payment_method: 'cash', + payment_method: 'lightning', description: `Payment from ${userBalance.username}`, reference: '', - loading: false + loading: false, + invoice: null, + paymentHash: null, + checkWalletKey: null, + pollIntervalId: null } }, + async generateSettlementInvoice() { + this.settleReceivableDialog.loading = true + + // Clear any existing polling interval + if (this.settleReceivableDialog.pollIntervalId) { + clearInterval(this.settleReceivableDialog.pollIntervalId) + this.settleReceivableDialog.pollIntervalId = null + } + + try { + // Generate an invoice on the Castle wallet for the user to pay + const response = await LNbits.api.request( + 'POST', + '/castle/api/v1/generate-payment-invoice', + this.g.user.wallets[0].adminkey, + { + amount: this.settleReceivableDialog.amount + } + ) + + // Store invoice details + this.settleReceivableDialog.invoice = response.data.payment_request + this.settleReceivableDialog.paymentHash = response.data.payment_hash + this.settleReceivableDialog.checkWalletKey = response.data.check_wallet_key + + this.$q.notify({ + type: 'positive', + message: 'Lightning invoice generated! User can scan QR code or copy to pay.', + timeout: 3000 + }) + + // Start polling for payment + this.pollForSettlementPayment( + response.data.payment_hash, + response.data.check_wallet_key + ) + } catch (error) { + LNbits.utils.notifyApiError(error) + } finally { + this.settleReceivableDialog.loading = false + } + }, + async pollForSettlementPayment(paymentHash, checkWalletKey) { + // Clear any existing interval + if (this.settleReceivableDialog.pollIntervalId) { + clearInterval(this.settleReceivableDialog.pollIntervalId) + } + + // Poll every 2 seconds for payment status + const checkPayment = async () => { + try { + const response = await LNbits.api.request( + 'GET', + `/api/v1/payments/${paymentHash}`, + checkWalletKey + ) + + if (response.data && response.data.paid) { + // Record payment in accounting - this creates the journal entry + // that settles the receivable + try { + await LNbits.api.request( + 'POST', + '/castle/api/v1/record-payment', + this.g.user.wallets[0].adminkey, + { + payment_hash: paymentHash + } + ) + } catch (error) { + console.error('Error recording settlement payment:', error) + } + + this.$q.notify({ + type: 'positive', + message: 'Payment received! Receivable has been settled.', + timeout: 3000 + }) + + // Close dialog and refresh + this.settleReceivableDialog.show = false + await this.loadBalance() + await this.loadTransactions() + await this.loadAllUserBalances() + return true + } + return false + } catch (error) { + // Silently ignore errors (payment might not exist yet) + return false + } + } + + // Check every 2 seconds for up to 5 minutes + let attempts = 0 + const maxAttempts = 150 // 5 minutes + this.settleReceivableDialog.pollIntervalId = setInterval(async () => { + attempts++ + const paid = await checkPayment() + if (paid || attempts >= maxAttempts) { + clearInterval(this.settleReceivableDialog.pollIntervalId) + this.settleReceivableDialog.pollIntervalId = null + } + }, 2000) + }, async submitSettleReceivable() { this.settleReceivableDialog.loading = true try { diff --git a/templates/castle/index.html b/templates/castle/index.html index 1cf8d8e..df33a88 100644 --- a/templates/castle/index.html +++ b/templates/castle/index.html @@ -1161,6 +1161,7 @@ dense v-model="settleReceivableDialog.payment_method" :options="[ + {label: 'Lightning Invoice', value: 'lightning'}, {label: 'Cash', value: 'cash'}, {label: 'Bank Transfer', value: 'bank_transfer'}, {label: 'Check', value: 'check'}, @@ -1174,7 +1175,46 @@ :rules="[val => !!val || 'Payment method is required']" > + +
+ + +
+
Lightning Invoice Generated
+ + + + + + +
+ Waiting for payment... +
+
+
+
- + + + Generate Invoice + + + + Settle Receivable + Cancel