Adds functionality to pay users (Castle pays)
Implements the ability for the super user (Castle) to pay other users for expenses or liabilities. Introduces a new `PayUser` model to represent these payments, along with API endpoints to process and record them. Integrates a "Pay User" button into the user list, allowing the super user to initiate payments through either lightning or manual methods (cash, bank transfer, check). Adds UI elements and logic for handling both lightning payments (generating invoices and paying them) and manual payment recording. This functionality allows Castle to manage and settle debts with its users directly through the application.
This commit is contained in:
parent
f0257e7c7f
commit
60aba90e00
4 changed files with 560 additions and 1 deletions
|
|
@ -99,6 +99,22 @@ window.app = Vue.createApp({
|
|||
pollIntervalId: null,
|
||||
exchangeRate: 3571.43, // sats per EUR (TODO: fetch from API)
|
||||
originalCurrency: 'BTC' // Track original receivable currency
|
||||
},
|
||||
payUserDialog: {
|
||||
show: false,
|
||||
user_id: '',
|
||||
username: '',
|
||||
maxAmount: 0,
|
||||
maxAmountFiat: 0,
|
||||
fiatCurrency: null,
|
||||
amount: 0,
|
||||
payment_method: 'lightning',
|
||||
description: '',
|
||||
reference: '',
|
||||
loading: false,
|
||||
paymentSuccess: false,
|
||||
exchangeRate: 3571.43,
|
||||
originalCurrency: 'BTC'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -135,6 +151,25 @@ window.app = Vue.createApp({
|
|||
else if (!isOldCash && isNewCash) {
|
||||
this.settleReceivableDialog.amount = this.settleReceivableDialog.maxAmountFiat || 0
|
||||
}
|
||||
},
|
||||
'payUserDialog.payment_method': function(newVal, oldVal) {
|
||||
// Convert amount when payment method changes between cash and lightning
|
||||
if (!oldVal) return
|
||||
|
||||
const isOldCash = ['cash', 'bank_transfer', 'check'].includes(oldVal)
|
||||
const isNewCash = ['cash', 'bank_transfer', 'check'].includes(newVal)
|
||||
|
||||
// Only convert if switching between cash and lightning
|
||||
if (isOldCash === isNewCash) return
|
||||
|
||||
// Convert from fiat to sats (when switching from cash to lightning)
|
||||
if (isOldCash && !isNewCash) {
|
||||
this.payUserDialog.amount = this.payUserDialog.maxAmount
|
||||
}
|
||||
// Convert from sats to fiat (when switching from lightning to cash)
|
||||
else if (!isOldCash && isNewCash) {
|
||||
this.payUserDialog.amount = this.payUserDialog.maxAmountFiat || 0
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -180,6 +215,30 @@ window.app = Vue.createApp({
|
|||
)
|
||||
return isCashPayment ? '0.01' : '1'
|
||||
},
|
||||
paymentAmountLabel() {
|
||||
const isCashPayment = ['cash', 'bank_transfer', 'check'].includes(
|
||||
this.payUserDialog.payment_method
|
||||
)
|
||||
if (isCashPayment && this.payUserDialog.fiatCurrency) {
|
||||
return `Payment Amount (${this.payUserDialog.fiatCurrency}) *`
|
||||
}
|
||||
return 'Payment Amount (sats) *'
|
||||
},
|
||||
paymentMaxAmount() {
|
||||
const isCashPayment = ['cash', 'bank_transfer', 'check'].includes(
|
||||
this.payUserDialog.payment_method
|
||||
)
|
||||
if (isCashPayment && this.payUserDialog.maxAmountFiat) {
|
||||
return this.payUserDialog.maxAmountFiat
|
||||
}
|
||||
return this.payUserDialog.maxAmount
|
||||
},
|
||||
paymentAmountStep() {
|
||||
const isCashPayment = ['cash', 'bank_transfer', 'check'].includes(
|
||||
this.payUserDialog.payment_method
|
||||
)
|
||||
return isCashPayment ? '0.01' : '1'
|
||||
},
|
||||
currencyOptions() {
|
||||
const options = [{label: 'Satoshis (default)', value: null}]
|
||||
this.currencies.forEach(curr => {
|
||||
|
|
@ -1132,6 +1191,172 @@ window.app = Vue.createApp({
|
|||
this.settleReceivableDialog.loading = false
|
||||
}
|
||||
},
|
||||
showPayUserDialog(userBalance) {
|
||||
// Only show for users castle owes (positive balance)
|
||||
if (userBalance.balance <= 0) return
|
||||
|
||||
// Extract fiat balances (e.g., EUR)
|
||||
const fiatBalances = userBalance.fiat_balances || {}
|
||||
const fiatCurrency = Object.keys(fiatBalances)[0] || null
|
||||
const fiatAmount = fiatCurrency ? fiatBalances[fiatCurrency] : 0
|
||||
|
||||
this.payUserDialog = {
|
||||
show: true,
|
||||
user_id: userBalance.user_id,
|
||||
username: userBalance.username,
|
||||
maxAmount: userBalance.balance, // Positive sats amount castle owes
|
||||
maxAmountFiat: fiatAmount, // EUR or other fiat amount
|
||||
fiatCurrency: fiatCurrency,
|
||||
amount: fiatCurrency ? fiatAmount : userBalance.balance, // Default to fiat if available
|
||||
payment_method: 'lightning', // Default to lightning for paying
|
||||
description: '',
|
||||
reference: '',
|
||||
loading: false,
|
||||
paymentSuccess: false,
|
||||
exchangeRate: fiatAmount > 0 ? userBalance.balance / fiatAmount : 3571.43,
|
||||
originalCurrency: fiatCurrency || 'BTC'
|
||||
}
|
||||
},
|
||||
async sendLightningPayment() {
|
||||
this.payUserDialog.loading = true
|
||||
try {
|
||||
// Request an invoice from the user's wallet
|
||||
const userWallet = await this.getUserWallet(this.payUserDialog.user_id)
|
||||
|
||||
if (!userWallet || !userWallet.user_wallet_id) {
|
||||
throw new Error('User has not configured their wallet. Ask them to set up their wallet first.')
|
||||
}
|
||||
|
||||
// Generate invoice on user's wallet
|
||||
const invoiceResponse = await LNbits.api.request(
|
||||
'POST',
|
||||
`/api/v1/payments`,
|
||||
userWallet.user_wallet_id_invoice_key,
|
||||
{
|
||||
out: false,
|
||||
amount: this.payUserDialog.amount,
|
||||
memo: `Payment from Castle to ${this.payUserDialog.username}`
|
||||
}
|
||||
)
|
||||
|
||||
const paymentRequest = invoiceResponse.data.payment_request
|
||||
|
||||
// Pay the invoice from Castle's wallet
|
||||
const paymentResponse = await LNbits.api.request(
|
||||
'POST',
|
||||
`/api/v1/payments`,
|
||||
this.g.user.wallets[0].adminkey,
|
||||
{
|
||||
out: true,
|
||||
bolt11: paymentRequest
|
||||
}
|
||||
)
|
||||
|
||||
// Record the payment in Castle accounting
|
||||
await LNbits.api.request(
|
||||
'POST',
|
||||
'/castle/api/v1/payables/pay',
|
||||
this.g.user.wallets[0].adminkey,
|
||||
{
|
||||
user_id: this.payUserDialog.user_id,
|
||||
amount: this.payUserDialog.amount,
|
||||
payment_method: 'lightning',
|
||||
payment_hash: paymentResponse.data.payment_hash
|
||||
}
|
||||
)
|
||||
|
||||
this.payUserDialog.paymentSuccess = true
|
||||
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: `Successfully sent ${this.formatSats(this.payUserDialog.amount)} sats to ${this.payUserDialog.username}`,
|
||||
timeout: 3000
|
||||
})
|
||||
|
||||
// Reload balances after a short delay
|
||||
setTimeout(async () => {
|
||||
await this.loadBalance()
|
||||
await this.loadTransactions()
|
||||
await this.loadAllUserBalances()
|
||||
this.payUserDialog.show = false
|
||||
}, 2000)
|
||||
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
} finally {
|
||||
this.payUserDialog.loading = false
|
||||
}
|
||||
},
|
||||
async submitPayUser() {
|
||||
this.payUserDialog.loading = true
|
||||
try {
|
||||
const isCashPayment = ['cash', 'bank_transfer', 'check'].includes(
|
||||
this.payUserDialog.payment_method
|
||||
)
|
||||
|
||||
// Create description with payment method
|
||||
const paymentMethodLabels = {
|
||||
'cash': 'Cash payment',
|
||||
'bank_transfer': 'Bank transfer',
|
||||
'check': 'Check payment',
|
||||
'other': 'Payment'
|
||||
}
|
||||
const methodLabel = paymentMethodLabels[this.payUserDialog.payment_method] || 'Payment'
|
||||
const description = this.payUserDialog.description ||
|
||||
`${methodLabel} to ${this.payUserDialog.username}`
|
||||
|
||||
const payload = {
|
||||
user_id: this.payUserDialog.user_id,
|
||||
amount: this.payUserDialog.amount,
|
||||
payment_method: this.payUserDialog.payment_method,
|
||||
description: description,
|
||||
reference: this.payUserDialog.reference || null,
|
||||
}
|
||||
|
||||
// Add currency info for fiat payments
|
||||
if (isCashPayment && this.payUserDialog.fiatCurrency) {
|
||||
payload.currency = this.payUserDialog.fiatCurrency
|
||||
payload.amount_sats = this.payUserDialog.maxAmount
|
||||
}
|
||||
|
||||
const response = await LNbits.api.request(
|
||||
'POST',
|
||||
'/castle/api/v1/payables/pay',
|
||||
this.g.user.wallets[0].adminkey,
|
||||
payload
|
||||
)
|
||||
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: response.data.message,
|
||||
timeout: 3000
|
||||
})
|
||||
|
||||
this.payUserDialog.show = false
|
||||
|
||||
// Reload balances
|
||||
await this.loadBalance()
|
||||
await this.loadTransactions()
|
||||
await this.loadAllUserBalances()
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
} finally {
|
||||
this.payUserDialog.loading = false
|
||||
}
|
||||
},
|
||||
async getUserWallet(userId) {
|
||||
try {
|
||||
const response = await LNbits.api.request(
|
||||
'GET',
|
||||
`/castle/api/v1/user-wallet/${userId}`,
|
||||
this.g.user.wallets[0].adminkey
|
||||
)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error('Error fetching user wallet:', error)
|
||||
return null
|
||||
}
|
||||
},
|
||||
formatSats(amount) {
|
||||
return new Intl.NumberFormat().format(amount)
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue