Adds lightning payment option for settling receivables
Implements the ability for users to settle their outstanding balance using a Lightning Network invoice. Generates an invoice on the Castle wallet and polls for payment, automatically recording the transaction once payment is detected. The UI is updated to display the invoice and handle the payment process.
This commit is contained in:
parent
1412359172
commit
a2a58d323b
2 changed files with 189 additions and 4 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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']"
|
||||
></q-select>
|
||||
|
||||
<!-- Show invoice if lightning method selected and generated -->
|
||||
<div v-if="settleReceivableDialog.payment_method === 'lightning' && settleReceivableDialog.invoice">
|
||||
<q-separator class="q-my-md"></q-separator>
|
||||
|
||||
<div class="text-center q-mb-md">
|
||||
<div class="text-subtitle2 q-mb-sm">Lightning Invoice Generated</div>
|
||||
<qrcode-vue
|
||||
:value="settleReceivableDialog.invoice"
|
||||
:size="280"
|
||||
level="M"
|
||||
class="q-mb-md"
|
||||
></qrcode-vue>
|
||||
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
readonly
|
||||
v-model="settleReceivableDialog.invoice"
|
||||
label="Invoice"
|
||||
>
|
||||
<template v-slot:append>
|
||||
<q-btn
|
||||
flat
|
||||
dense
|
||||
icon="content_copy"
|
||||
@click="copyToClipboard(settleReceivableDialog.invoice)"
|
||||
>
|
||||
<q-tooltip>Copy invoice</q-tooltip>
|
||||
</q-btn>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div class="text-caption text-grey q-mt-sm">
|
||||
Waiting for payment...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<q-input
|
||||
v-if="settleReceivableDialog.payment_method !== 'lightning'"
|
||||
filled
|
||||
dense
|
||||
v-model="settleReceivableDialog.description"
|
||||
|
|
@ -1185,6 +1225,7 @@
|
|||
></q-input>
|
||||
|
||||
<q-input
|
||||
v-if="settleReceivableDialog.payment_method !== 'lightning'"
|
||||
filled
|
||||
dense
|
||||
v-model="settleReceivableDialog.reference"
|
||||
|
|
@ -1194,9 +1235,28 @@
|
|||
></q-input>
|
||||
|
||||
<div class="row q-mt-md q-gutter-sm">
|
||||
<q-btn unelevated color="primary" type="submit" :loading="settleReceivableDialog.loading">
|
||||
<!-- For lightning: generate invoice button, then it auto-settles on payment -->
|
||||
<q-btn
|
||||
v-if="settleReceivableDialog.payment_method === 'lightning' && !settleReceivableDialog.invoice"
|
||||
unelevated
|
||||
color="primary"
|
||||
@click="generateSettlementInvoice"
|
||||
:loading="settleReceivableDialog.loading"
|
||||
>
|
||||
Generate Invoice
|
||||
</q-btn>
|
||||
|
||||
<!-- For non-lightning: manual settle button -->
|
||||
<q-btn
|
||||
v-if="settleReceivableDialog.payment_method !== 'lightning'"
|
||||
unelevated
|
||||
color="primary"
|
||||
type="submit"
|
||||
:loading="settleReceivableDialog.loading"
|
||||
>
|
||||
Settle Receivable
|
||||
</q-btn>
|
||||
|
||||
<q-btn v-close-popup flat color="grey">Cancel</q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue