Uses usernames instead of user IDs in the audit log and approval request listings to improve readability. If a user is not found, it displays a truncated version of the user ID.
1475 lines
48 KiB
JavaScript
1475 lines
48 KiB
JavaScript
const mapJournalEntry = obj => {
|
|
return obj
|
|
}
|
|
|
|
window.app = Vue.createApp({
|
|
el: '#vue',
|
|
mixins: [windowMixin],
|
|
data() {
|
|
return {
|
|
balance: null,
|
|
allUserBalances: [],
|
|
transactions: [],
|
|
accounts: [],
|
|
currencies: [],
|
|
users: [],
|
|
settings: null,
|
|
userWalletSettings: null,
|
|
isAdmin: false,
|
|
isSuperUser: false,
|
|
castleWalletConfigured: false,
|
|
userWalletConfigured: false,
|
|
currentExchangeRate: null, // BTC/EUR rate (sats per EUR)
|
|
expenseDialog: {
|
|
show: false,
|
|
description: '',
|
|
amount: null,
|
|
expenseAccount: '',
|
|
isEquity: false,
|
|
reference: '',
|
|
currency: 'EUR',
|
|
date: new Date().toISOString().split('T')[0], // YYYY-MM-DD format
|
|
loading: false
|
|
},
|
|
payDialog: {
|
|
show: false,
|
|
amount: null,
|
|
paymentRequest: null,
|
|
paymentHash: null,
|
|
checkWalletKey: null,
|
|
pollIntervalId: null,
|
|
loading: false
|
|
},
|
|
settingsDialog: {
|
|
show: false,
|
|
castleWalletId: '',
|
|
loading: false
|
|
},
|
|
userWalletDialog: {
|
|
show: false,
|
|
userWalletId: '',
|
|
loading: false
|
|
},
|
|
receivableDialog: {
|
|
show: false,
|
|
selectedUser: '',
|
|
description: '',
|
|
amount: null,
|
|
revenueAccount: '',
|
|
reference: '',
|
|
currency: null,
|
|
loading: false
|
|
},
|
|
manualPaymentDialog: {
|
|
show: false,
|
|
amount: null,
|
|
description: '',
|
|
loading: false
|
|
},
|
|
manualPaymentRequests: [],
|
|
pendingExpenses: [],
|
|
balanceAssertions: [],
|
|
assertionDialog: {
|
|
show: false,
|
|
account_id: '',
|
|
expected_balance_sats: 0,
|
|
expected_balance_fiat: null,
|
|
fiat_currency: null,
|
|
tolerance_sats: 0,
|
|
tolerance_fiat: 0,
|
|
loading: false
|
|
},
|
|
reconciliation: {
|
|
summary: null,
|
|
discrepancies: null,
|
|
checking: false,
|
|
showDiscrepancies: false
|
|
},
|
|
settleReceivableDialog: {
|
|
show: false,
|
|
user_id: '',
|
|
username: '',
|
|
maxAmount: 0,
|
|
amount: 0,
|
|
payment_method: 'cash',
|
|
description: '',
|
|
reference: '',
|
|
loading: false,
|
|
invoice: null,
|
|
paymentHash: null,
|
|
checkWalletKey: null,
|
|
pollIntervalId: null,
|
|
exchangeRate: null, // Will be set from currentExchangeRate
|
|
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: null, // Will be set from currentExchangeRate
|
|
originalCurrency: 'BTC'
|
|
}
|
|
}
|
|
},
|
|
watch: {
|
|
'payDialog.show': function(newVal) {
|
|
// When dialog is closed, stop polling
|
|
if (!newVal && this.payDialog.pollIntervalId) {
|
|
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
|
|
}
|
|
},
|
|
'settleReceivableDialog.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.settleReceivableDialog.amount = this.settleReceivableDialog.maxAmount
|
|
}
|
|
// Convert from sats to fiat (when switching from lightning to cash)
|
|
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: {
|
|
expenseAccounts() {
|
|
return this.accounts.filter(a => a.account_type === 'expense')
|
|
},
|
|
revenueAccounts() {
|
|
return this.accounts.filter(a => a.account_type === 'revenue')
|
|
},
|
|
amountLabel() {
|
|
if (this.expenseDialog.currency) {
|
|
return `Amount (${this.expenseDialog.currency}) *`
|
|
}
|
|
return 'Amount (sats) *'
|
|
},
|
|
receivableAmountLabel() {
|
|
if (this.receivableDialog.currency) {
|
|
return `Amount (${this.receivableDialog.currency}) *`
|
|
}
|
|
return 'Amount (sats) *'
|
|
},
|
|
settlementAmountLabel() {
|
|
const isCashPayment = ['cash', 'bank_transfer', 'check'].includes(
|
|
this.settleReceivableDialog.payment_method
|
|
)
|
|
if (isCashPayment && this.settleReceivableDialog.fiatCurrency) {
|
|
return `Settlement Amount (${this.settleReceivableDialog.fiatCurrency}) *`
|
|
}
|
|
return 'Settlement Amount (sats) *'
|
|
},
|
|
settlementMaxAmount() {
|
|
const isCashPayment = ['cash', 'bank_transfer', 'check'].includes(
|
|
this.settleReceivableDialog.payment_method
|
|
)
|
|
if (isCashPayment && this.settleReceivableDialog.maxAmountFiat) {
|
|
return this.settleReceivableDialog.maxAmountFiat
|
|
}
|
|
return this.settleReceivableDialog.maxAmount
|
|
},
|
|
settlementAmountStep() {
|
|
const isCashPayment = ['cash', 'bank_transfer', 'check'].includes(
|
|
this.settleReceivableDialog.payment_method
|
|
)
|
|
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 => {
|
|
options.push({label: curr, value: curr})
|
|
})
|
|
return options
|
|
},
|
|
userOptions() {
|
|
const options = []
|
|
this.users.forEach(user => {
|
|
options.push({
|
|
label: user.username,
|
|
value: user.user_id
|
|
})
|
|
})
|
|
return options
|
|
},
|
|
pendingManualPaymentRequests() {
|
|
return this.manualPaymentRequests.filter(r => r.status === 'pending')
|
|
},
|
|
failedAssertions() {
|
|
return this.balanceAssertions.filter(a => a.status === 'failed')
|
|
},
|
|
outstandingUserBalances() {
|
|
// Filter to only show users with non-zero balances
|
|
return this.allUserBalances.filter(user => user.balance !== 0)
|
|
},
|
|
passedAssertions() {
|
|
return this.balanceAssertions.filter(a => a.status === 'passed')
|
|
},
|
|
allAccounts() {
|
|
return this.accounts
|
|
}
|
|
},
|
|
methods: {
|
|
async loadBalance() {
|
|
try {
|
|
const response = await LNbits.api.request(
|
|
'GET',
|
|
'/castle/api/v1/balance',
|
|
this.g.user.wallets[0].inkey
|
|
)
|
|
this.balance = response.data
|
|
|
|
// If super user, also load all user balances
|
|
if (this.isSuperUser) {
|
|
await this.loadAllUserBalances()
|
|
}
|
|
} catch (error) {
|
|
LNbits.utils.notifyApiError(error)
|
|
}
|
|
},
|
|
async loadAllUserBalances() {
|
|
try {
|
|
const response = await LNbits.api.request(
|
|
'GET',
|
|
'/castle/api/v1/balances/all',
|
|
this.g.user.wallets[0].adminkey
|
|
)
|
|
this.allUserBalances = response.data
|
|
} catch (error) {
|
|
console.error('Error loading all user balances:', error)
|
|
}
|
|
},
|
|
async loadTransactions() {
|
|
try {
|
|
const response = await LNbits.api.request(
|
|
'GET',
|
|
'/castle/api/v1/entries/user',
|
|
this.g.user.wallets[0].inkey
|
|
)
|
|
this.transactions = response.data
|
|
} catch (error) {
|
|
LNbits.utils.notifyApiError(error)
|
|
}
|
|
},
|
|
async loadAccounts() {
|
|
try {
|
|
const response = await LNbits.api.request(
|
|
'GET',
|
|
'/castle/api/v1/accounts',
|
|
this.g.user.wallets[0].inkey
|
|
)
|
|
this.accounts = response.data
|
|
} catch (error) {
|
|
LNbits.utils.notifyApiError(error)
|
|
}
|
|
},
|
|
async loadCurrencies() {
|
|
try {
|
|
const response = await LNbits.api.request(
|
|
'GET',
|
|
'/castle/api/v1/currencies',
|
|
this.g.user.wallets[0].inkey
|
|
)
|
|
this.currencies = response.data
|
|
} catch (error) {
|
|
LNbits.utils.notifyApiError(error)
|
|
}
|
|
},
|
|
async loadUsers() {
|
|
try {
|
|
const response = await LNbits.api.request(
|
|
'GET',
|
|
'/castle/api/v1/users',
|
|
this.g.user.wallets[0].adminkey
|
|
)
|
|
this.users = response.data
|
|
} catch (error) {
|
|
console.error('Error loading users:', error)
|
|
}
|
|
},
|
|
async loadSettings() {
|
|
try {
|
|
// Try with admin key first to check settings
|
|
const response = await LNbits.api.request(
|
|
'GET',
|
|
'/castle/api/v1/settings',
|
|
this.g.user.wallets[0].inkey
|
|
)
|
|
this.settings = response.data
|
|
this.castleWalletConfigured = !!(this.settings && this.settings.castle_wallet_id)
|
|
|
|
// Check if user is super user by seeing if they can access admin features
|
|
this.isSuperUser = this.g.user.super_user || false
|
|
this.isAdmin = this.g.user.admin || this.isSuperUser
|
|
} catch (error) {
|
|
// Settings not available
|
|
this.castleWalletConfigured = false
|
|
}
|
|
},
|
|
async loadUserWallet() {
|
|
try {
|
|
const response = await LNbits.api.request(
|
|
'GET',
|
|
'/castle/api/v1/user/wallet',
|
|
this.g.user.wallets[0].inkey
|
|
)
|
|
this.userWalletSettings = response.data
|
|
this.userWalletConfigured = !!(this.userWalletSettings && this.userWalletSettings.user_wallet_id)
|
|
} catch (error) {
|
|
this.userWalletConfigured = false
|
|
}
|
|
},
|
|
showSettingsDialog() {
|
|
this.settingsDialog.castleWalletId = this.settings?.castle_wallet_id || ''
|
|
this.settingsDialog.show = true
|
|
},
|
|
showUserWalletDialog() {
|
|
this.userWalletDialog.userWalletId = this.userWalletSettings?.user_wallet_id || ''
|
|
this.userWalletDialog.show = true
|
|
},
|
|
async submitSettings() {
|
|
if (!this.settingsDialog.castleWalletId) {
|
|
this.$q.notify({
|
|
type: 'warning',
|
|
message: 'Castle Wallet ID is required'
|
|
})
|
|
return
|
|
}
|
|
|
|
this.settingsDialog.loading = true
|
|
try {
|
|
await LNbits.api.request(
|
|
'PUT',
|
|
'/castle/api/v1/settings',
|
|
this.g.user.wallets[0].adminkey,
|
|
{
|
|
castle_wallet_id: this.settingsDialog.castleWalletId
|
|
}
|
|
)
|
|
this.$q.notify({
|
|
type: 'positive',
|
|
message: 'Settings updated successfully'
|
|
})
|
|
this.settingsDialog.show = false
|
|
await this.loadSettings()
|
|
// Reload user wallet to reflect castle wallet for super user
|
|
if (this.isSuperUser) {
|
|
await this.loadUserWallet()
|
|
}
|
|
} catch (error) {
|
|
LNbits.utils.notifyApiError(error)
|
|
} finally {
|
|
this.settingsDialog.loading = false
|
|
}
|
|
},
|
|
async submitUserWallet() {
|
|
if (!this.userWalletDialog.userWalletId) {
|
|
this.$q.notify({
|
|
type: 'warning',
|
|
message: 'Wallet ID is required'
|
|
})
|
|
return
|
|
}
|
|
|
|
this.userWalletDialog.loading = true
|
|
try {
|
|
await LNbits.api.request(
|
|
'PUT',
|
|
'/castle/api/v1/user/wallet',
|
|
this.g.user.wallets[0].inkey,
|
|
{
|
|
user_wallet_id: this.userWalletDialog.userWalletId
|
|
}
|
|
)
|
|
this.$q.notify({
|
|
type: 'positive',
|
|
message: 'Wallet configured successfully'
|
|
})
|
|
this.userWalletDialog.show = false
|
|
await this.loadUserWallet()
|
|
} catch (error) {
|
|
LNbits.utils.notifyApiError(error)
|
|
} finally {
|
|
this.userWalletDialog.loading = false
|
|
}
|
|
},
|
|
async submitExpense() {
|
|
this.expenseDialog.loading = true
|
|
try {
|
|
await LNbits.api.request(
|
|
'POST',
|
|
'/castle/api/v1/entries/expense',
|
|
this.g.user.wallets[0].inkey,
|
|
{
|
|
description: this.expenseDialog.description,
|
|
amount: this.expenseDialog.amount,
|
|
expense_account: this.expenseDialog.expenseAccount,
|
|
is_equity: this.expenseDialog.isEquity,
|
|
user_wallet: this.g.user.wallets[0].id,
|
|
reference: this.expenseDialog.reference || null,
|
|
currency: this.expenseDialog.currency || null,
|
|
entry_date: this.expenseDialog.date ? `${this.expenseDialog.date}T00:00:00` : null
|
|
}
|
|
)
|
|
this.$q.notify({
|
|
type: 'positive',
|
|
message: 'Expense added successfully'
|
|
})
|
|
this.expenseDialog.show = false
|
|
this.resetExpenseDialog()
|
|
await this.loadBalance()
|
|
await this.loadTransactions()
|
|
} catch (error) {
|
|
LNbits.utils.notifyApiError(error)
|
|
} finally {
|
|
this.expenseDialog.loading = false
|
|
}
|
|
},
|
|
async submitPayment() {
|
|
this.payDialog.loading = true
|
|
|
|
// Clear any existing polling interval
|
|
if (this.payDialog.pollIntervalId) {
|
|
clearInterval(this.payDialog.pollIntervalId)
|
|
this.payDialog.pollIntervalId = null
|
|
}
|
|
|
|
try {
|
|
// Generate an invoice on the Castle wallet
|
|
const response = await LNbits.api.request(
|
|
'POST',
|
|
'/castle/api/v1/generate-payment-invoice',
|
|
this.g.user.wallets[0].inkey,
|
|
{
|
|
amount: this.payDialog.amount
|
|
}
|
|
)
|
|
|
|
// Show the payment request in the dialog
|
|
this.payDialog.paymentRequest = response.data.payment_request
|
|
this.payDialog.paymentHash = response.data.payment_hash
|
|
this.payDialog.checkWalletKey = response.data.check_wallet_key
|
|
|
|
this.$q.notify({
|
|
type: 'positive',
|
|
message: 'Invoice generated! Scan QR code or copy to pay.',
|
|
timeout: 3000
|
|
})
|
|
|
|
// Poll for payment completion
|
|
this.pollForPayment(response.data.payment_hash, response.data.check_wallet_key)
|
|
} catch (error) {
|
|
LNbits.utils.notifyApiError(error)
|
|
} finally {
|
|
this.payDialog.loading = false
|
|
}
|
|
},
|
|
async pollForPayment(paymentHash, checkWalletKey) {
|
|
// Clear any existing interval
|
|
if (this.payDialog.pollIntervalId) {
|
|
clearInterval(this.payDialog.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
|
|
try {
|
|
await LNbits.api.request(
|
|
'POST',
|
|
'/castle/api/v1/record-payment',
|
|
this.g.user.wallets[0].inkey,
|
|
{
|
|
payment_hash: paymentHash
|
|
}
|
|
)
|
|
} catch (error) {
|
|
console.error('Error recording payment:', error)
|
|
}
|
|
|
|
this.$q.notify({
|
|
type: 'positive',
|
|
message: 'Payment received! Your balance has been updated.',
|
|
timeout: 3000
|
|
})
|
|
this.payDialog.show = false
|
|
this.payDialog.paymentRequest = null
|
|
this.payDialog.amount = null
|
|
await this.loadBalance()
|
|
await this.loadTransactions()
|
|
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.payDialog.pollIntervalId = setInterval(async () => {
|
|
attempts++
|
|
const paid = await checkPayment()
|
|
if (paid || attempts >= maxAttempts) {
|
|
clearInterval(this.payDialog.pollIntervalId)
|
|
this.payDialog.pollIntervalId = null
|
|
}
|
|
}, 2000)
|
|
},
|
|
showManualPaymentOption() {
|
|
// This is for when user wants to pay their debt manually
|
|
// For now, just notify them to contact castle
|
|
this.$q.notify({
|
|
type: 'info',
|
|
message: 'Please contact Castle directly to arrange manual payment.',
|
|
timeout: 3000
|
|
})
|
|
},
|
|
showManualPaymentDialog() {
|
|
// This is for when Castle owes user and they want to request manual payment
|
|
this.manualPaymentDialog.amount = Math.abs(this.balance.balance)
|
|
this.manualPaymentDialog.description = ''
|
|
this.manualPaymentDialog.show = true
|
|
},
|
|
async submitManualPaymentRequest() {
|
|
this.manualPaymentDialog.loading = true
|
|
try {
|
|
await LNbits.api.request(
|
|
'POST',
|
|
'/castle/api/v1/manual-payment-request',
|
|
this.g.user.wallets[0].inkey,
|
|
{
|
|
amount: this.manualPaymentDialog.amount,
|
|
description: this.manualPaymentDialog.description
|
|
}
|
|
)
|
|
this.$q.notify({
|
|
type: 'positive',
|
|
message: 'Manual payment request submitted successfully!'
|
|
})
|
|
this.manualPaymentDialog.show = false
|
|
this.manualPaymentDialog.amount = null
|
|
this.manualPaymentDialog.description = ''
|
|
await this.loadManualPaymentRequests()
|
|
} catch (error) {
|
|
LNbits.utils.notifyApiError(error)
|
|
} finally {
|
|
this.manualPaymentDialog.loading = false
|
|
}
|
|
},
|
|
async loadManualPaymentRequests() {
|
|
try {
|
|
// If super user, load all requests; otherwise load user's own requests
|
|
const endpoint = this.isSuperUser
|
|
? '/castle/api/v1/manual-payment-requests/all'
|
|
: '/castle/api/v1/manual-payment-requests'
|
|
const key = this.isSuperUser
|
|
? this.g.user.wallets[0].adminkey
|
|
: this.g.user.wallets[0].inkey
|
|
|
|
const response = await LNbits.api.request(
|
|
'GET',
|
|
endpoint,
|
|
key,
|
|
this.isSuperUser ? {status: 'pending'} : {}
|
|
)
|
|
this.manualPaymentRequests = response.data
|
|
} catch (error) {
|
|
console.error('Error loading manual payment requests:', error)
|
|
}
|
|
},
|
|
async loadPendingExpenses() {
|
|
try {
|
|
if (!this.isSuperUser) return
|
|
if (!this.g.user.wallets[0]?.adminkey) return
|
|
|
|
const response = await LNbits.api.request(
|
|
'GET',
|
|
'/castle/api/v1/entries/pending',
|
|
this.g.user.wallets[0].adminkey
|
|
)
|
|
this.pendingExpenses = response.data
|
|
} catch (error) {
|
|
console.error('Error loading pending expenses:', error)
|
|
}
|
|
},
|
|
async approveManualPaymentRequest(requestId) {
|
|
try {
|
|
await LNbits.api.request(
|
|
'POST',
|
|
`/castle/api/v1/manual-payment-requests/${requestId}/approve`,
|
|
this.g.user.wallets[0].adminkey
|
|
)
|
|
this.$q.notify({
|
|
type: 'positive',
|
|
message: 'Manual payment request approved!'
|
|
})
|
|
await this.loadManualPaymentRequests()
|
|
await this.loadBalance()
|
|
await this.loadTransactions()
|
|
} catch (error) {
|
|
LNbits.utils.notifyApiError(error)
|
|
}
|
|
},
|
|
async rejectManualPaymentRequest(requestId) {
|
|
try {
|
|
await LNbits.api.request(
|
|
'POST',
|
|
`/castle/api/v1/manual-payment-requests/${requestId}/reject`,
|
|
this.g.user.wallets[0].adminkey
|
|
)
|
|
this.$q.notify({
|
|
type: 'warning',
|
|
message: 'Manual payment request rejected'
|
|
})
|
|
await this.loadManualPaymentRequests()
|
|
} catch (error) {
|
|
LNbits.utils.notifyApiError(error)
|
|
}
|
|
},
|
|
async approveExpense(entryId) {
|
|
try {
|
|
await LNbits.api.request(
|
|
'POST',
|
|
`/castle/api/v1/entries/${entryId}/approve`,
|
|
this.g.user.wallets[0].adminkey
|
|
)
|
|
this.$q.notify({
|
|
type: 'positive',
|
|
message: 'Expense approved!'
|
|
})
|
|
await this.loadPendingExpenses()
|
|
await this.loadBalance()
|
|
await this.loadTransactions()
|
|
await this.loadAllUserBalances()
|
|
} catch (error) {
|
|
LNbits.utils.notifyApiError(error)
|
|
}
|
|
},
|
|
async rejectExpense(entryId) {
|
|
try {
|
|
await LNbits.api.request(
|
|
'POST',
|
|
`/castle/api/v1/entries/${entryId}/reject`,
|
|
this.g.user.wallets[0].adminkey
|
|
)
|
|
this.$q.notify({
|
|
type: 'warning',
|
|
message: 'Expense rejected'
|
|
})
|
|
await this.loadPendingExpenses()
|
|
await this.loadTransactions()
|
|
} catch (error) {
|
|
LNbits.utils.notifyApiError(error)
|
|
}
|
|
},
|
|
async loadBalanceAssertions() {
|
|
if (!this.isSuperUser) return
|
|
|
|
try {
|
|
const response = await LNbits.api.request(
|
|
'GET',
|
|
'/castle/api/v1/assertions',
|
|
this.g.user.wallets[0].adminkey
|
|
)
|
|
this.balanceAssertions = response.data
|
|
} catch (error) {
|
|
LNbits.utils.notifyApiError(error)
|
|
}
|
|
},
|
|
async submitAssertion() {
|
|
this.assertionDialog.loading = true
|
|
try {
|
|
const payload = {
|
|
account_id: this.assertionDialog.account_id,
|
|
expected_balance_sats: this.assertionDialog.expected_balance_sats,
|
|
tolerance_sats: this.assertionDialog.tolerance_sats || 0
|
|
}
|
|
|
|
// Add fiat balance check if currency selected
|
|
if (this.assertionDialog.fiat_currency) {
|
|
payload.fiat_currency = this.assertionDialog.fiat_currency
|
|
payload.expected_balance_fiat = this.assertionDialog.expected_balance_fiat
|
|
payload.tolerance_fiat = this.assertionDialog.tolerance_fiat || 0
|
|
}
|
|
|
|
await LNbits.api.request(
|
|
'POST',
|
|
'/castle/api/v1/assertions',
|
|
this.g.user.wallets[0].adminkey,
|
|
payload
|
|
)
|
|
|
|
this.$q.notify({
|
|
type: 'positive',
|
|
message: 'Balance assertion passed!',
|
|
timeout: 3000
|
|
})
|
|
|
|
// Reset dialog
|
|
this.assertionDialog = {
|
|
show: false,
|
|
account_id: '',
|
|
expected_balance_sats: 0,
|
|
expected_balance_fiat: null,
|
|
fiat_currency: null,
|
|
tolerance_sats: 0,
|
|
tolerance_fiat: 0,
|
|
loading: false
|
|
}
|
|
|
|
// Reload assertions
|
|
await this.loadBalanceAssertions()
|
|
} catch (error) {
|
|
// Check if it's a 409 Conflict (assertion failed)
|
|
if (error.response && error.response.status === 409) {
|
|
const detail = error.response.data.detail
|
|
this.$q.notify({
|
|
type: 'negative',
|
|
message: `Assertion Failed! Expected: ${this.formatSats(detail.expected_sats)} sats, Got: ${this.formatSats(detail.actual_sats)} sats (diff: ${this.formatSats(detail.difference_sats)} sats)`,
|
|
timeout: 5000,
|
|
html: true
|
|
})
|
|
// Still reload to show the failed assertion
|
|
await this.loadBalanceAssertions()
|
|
this.assertionDialog.show = false
|
|
} else {
|
|
LNbits.utils.notifyApiError(error)
|
|
}
|
|
} finally {
|
|
this.assertionDialog.loading = false
|
|
}
|
|
},
|
|
async recheckAssertion(assertionId) {
|
|
try {
|
|
await LNbits.api.request(
|
|
'POST',
|
|
`/castle/api/v1/assertions/${assertionId}/check`,
|
|
this.g.user.wallets[0].adminkey
|
|
)
|
|
|
|
this.$q.notify({
|
|
type: 'positive',
|
|
message: 'Assertion re-checked',
|
|
timeout: 2000
|
|
})
|
|
|
|
await this.loadBalanceAssertions()
|
|
} catch (error) {
|
|
LNbits.utils.notifyApiError(error)
|
|
}
|
|
},
|
|
async deleteAssertion(assertionId) {
|
|
try {
|
|
await LNbits.api.request(
|
|
'DELETE',
|
|
`/castle/api/v1/assertions/${assertionId}`,
|
|
this.g.user.wallets[0].adminkey
|
|
)
|
|
|
|
this.$q.notify({
|
|
type: 'positive',
|
|
message: 'Assertion deleted',
|
|
timeout: 2000
|
|
})
|
|
|
|
await this.loadBalanceAssertions()
|
|
} catch (error) {
|
|
LNbits.utils.notifyApiError(error)
|
|
}
|
|
},
|
|
getAccountName(accountId) {
|
|
const account = this.accounts.find(a => a.id === accountId)
|
|
return account ? account.name : accountId
|
|
},
|
|
getUserName(userId) {
|
|
const user = this.users.find(u => u.user_id === userId)
|
|
return user ? user.username : userId.substring(0, 16) + '...'
|
|
},
|
|
async loadReconciliationSummary() {
|
|
if (!this.isSuperUser) return
|
|
|
|
try {
|
|
const response = await LNbits.api.request(
|
|
'GET',
|
|
'/castle/api/v1/reconciliation/summary',
|
|
this.g.user.wallets[0].adminkey
|
|
)
|
|
this.reconciliation.summary = response.data
|
|
} catch (error) {
|
|
LNbits.utils.notifyApiError(error)
|
|
}
|
|
},
|
|
async loadReconciliationDiscrepancies() {
|
|
if (!this.isSuperUser) return
|
|
|
|
try {
|
|
const response = await LNbits.api.request(
|
|
'GET',
|
|
'/castle/api/v1/reconciliation/discrepancies',
|
|
this.g.user.wallets[0].adminkey
|
|
)
|
|
this.reconciliation.discrepancies = response.data
|
|
} catch (error) {
|
|
LNbits.utils.notifyApiError(error)
|
|
}
|
|
},
|
|
async runFullReconciliation() {
|
|
this.reconciliation.checking = true
|
|
try {
|
|
const response = await LNbits.api.request(
|
|
'POST',
|
|
'/castle/api/v1/reconciliation/check-all',
|
|
this.g.user.wallets[0].adminkey
|
|
)
|
|
|
|
const results = response.data
|
|
this.$q.notify({
|
|
type: results.failed > 0 ? 'warning' : 'positive',
|
|
message: `Checked ${results.checked} assertions: ${results.passed} passed, ${results.failed} failed`,
|
|
timeout: 3000
|
|
})
|
|
|
|
// Reload reconciliation data
|
|
await this.loadReconciliationSummary()
|
|
await this.loadReconciliationDiscrepancies()
|
|
await this.loadBalanceAssertions()
|
|
} catch (error) {
|
|
LNbits.utils.notifyApiError(error)
|
|
} finally {
|
|
this.reconciliation.checking = false
|
|
}
|
|
},
|
|
copyToClipboard(text) {
|
|
navigator.clipboard.writeText(text)
|
|
this.$q.notify({
|
|
type: 'positive',
|
|
message: 'Copied to clipboard!',
|
|
timeout: 1000
|
|
})
|
|
},
|
|
showPayBalanceDialog() {
|
|
// Clear any existing polling
|
|
if (this.payDialog.pollIntervalId) {
|
|
clearInterval(this.payDialog.pollIntervalId)
|
|
this.payDialog.pollIntervalId = null
|
|
}
|
|
|
|
this.payDialog.amount = Math.abs(this.balance.balance)
|
|
this.payDialog.paymentRequest = null
|
|
this.payDialog.paymentHash = null
|
|
this.payDialog.checkWalletKey = null
|
|
this.payDialog.show = true
|
|
},
|
|
async showReceivableDialog() {
|
|
// Load users if not already loaded
|
|
if (this.users.length === 0 && this.isSuperUser) {
|
|
await this.loadUsers()
|
|
}
|
|
this.receivableDialog.show = true
|
|
},
|
|
async submitReceivable() {
|
|
this.receivableDialog.loading = true
|
|
try {
|
|
await LNbits.api.request(
|
|
'POST',
|
|
'/castle/api/v1/entries/receivable',
|
|
this.g.user.wallets[0].adminkey,
|
|
{
|
|
description: this.receivableDialog.description,
|
|
amount: this.receivableDialog.amount,
|
|
revenue_account: this.receivableDialog.revenueAccount,
|
|
user_id: this.receivableDialog.selectedUser,
|
|
reference: this.receivableDialog.reference || null,
|
|
currency: this.receivableDialog.currency || null
|
|
}
|
|
)
|
|
this.$q.notify({
|
|
type: 'positive',
|
|
message: 'Receivable added successfully'
|
|
})
|
|
this.receivableDialog.show = false
|
|
this.resetReceivableDialog()
|
|
await this.loadBalance()
|
|
await this.loadTransactions()
|
|
} catch (error) {
|
|
LNbits.utils.notifyApiError(error)
|
|
} finally {
|
|
this.receivableDialog.loading = false
|
|
}
|
|
},
|
|
resetExpenseDialog() {
|
|
this.expenseDialog.description = ''
|
|
this.expenseDialog.amount = null
|
|
this.expenseDialog.expenseAccount = ''
|
|
this.expenseDialog.isEquity = false
|
|
this.expenseDialog.reference = ''
|
|
this.expenseDialog.currency = 'EUR'
|
|
this.expenseDialog.date = new Date().toISOString().split('T')[0]
|
|
},
|
|
resetReceivableDialog() {
|
|
this.receivableDialog.selectedUser = ''
|
|
this.receivableDialog.description = ''
|
|
this.receivableDialog.amount = null
|
|
this.receivableDialog.revenueAccount = ''
|
|
this.receivableDialog.reference = ''
|
|
this.receivableDialog.currency = null
|
|
},
|
|
showSettleReceivableDialog(userBalance) {
|
|
// 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)
|
|
}
|
|
|
|
// Extract fiat balances (e.g., EUR)
|
|
const fiatBalances = userBalance.fiat_balances || {}
|
|
const fiatCurrency = Object.keys(fiatBalances)[0] || null // Get first fiat currency (e.g., 'EUR')
|
|
const fiatAmount = fiatCurrency ? Math.abs(fiatBalances[fiatCurrency]) : 0
|
|
|
|
this.settleReceivableDialog = {
|
|
show: true,
|
|
user_id: userBalance.user_id,
|
|
username: userBalance.username,
|
|
maxAmount: Math.abs(userBalance.balance), // Sats amount
|
|
maxAmountFiat: fiatAmount, // EUR or other fiat amount
|
|
fiatCurrency: fiatCurrency, // 'EUR', 'USD', etc.
|
|
amount: fiatCurrency ? fiatAmount : Math.abs(userBalance.balance), // Default to fiat if available, otherwise sats
|
|
payment_method: fiatCurrency ? 'cash' : 'lightning', // Default to cash if fiat balance exists
|
|
description: '', // Will be auto-generated based on payment method if left empty
|
|
reference: '',
|
|
loading: false,
|
|
invoice: null,
|
|
paymentHash: null,
|
|
checkWalletKey: null,
|
|
pollIntervalId: null,
|
|
exchangeRate: fiatAmount > 0 ? Math.abs(userBalance.balance) / fiatAmount : this.currentExchangeRate, // Calculate rate from actual amounts or use current rate
|
|
originalCurrency: fiatCurrency || 'BTC'
|
|
}
|
|
},
|
|
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,
|
|
user_id: this.settleReceivableDialog.user_id // Specify which user this invoice is for
|
|
}
|
|
)
|
|
|
|
// 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) {
|
|
// Stop polling immediately
|
|
clearInterval(this.settleReceivableDialog.pollIntervalId)
|
|
this.settleReceivableDialog.pollIntervalId = null
|
|
|
|
// Record payment in accounting - this creates the journal entry
|
|
// that settles the receivable
|
|
try {
|
|
const recordResponse = await LNbits.api.request(
|
|
'POST',
|
|
'/castle/api/v1/record-payment',
|
|
this.g.user.wallets[0].adminkey,
|
|
{
|
|
payment_hash: paymentHash
|
|
}
|
|
)
|
|
console.log('Settlement payment recorded:', recordResponse.data)
|
|
|
|
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()
|
|
} catch (error) {
|
|
console.error('Error recording settlement payment:', error)
|
|
this.$q.notify({
|
|
type: 'negative',
|
|
message: 'Payment detected but failed to record: ' + (error.response?.data?.detail || error.message),
|
|
timeout: 5000
|
|
})
|
|
}
|
|
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 {
|
|
// Determine if this is a fiat payment
|
|
const isCashPayment = ['cash', 'bank_transfer', 'check'].includes(
|
|
this.settleReceivableDialog.payment_method
|
|
)
|
|
|
|
// Create description with payment method
|
|
const paymentMethodLabels = {
|
|
'cash': 'Cash payment',
|
|
'bank_transfer': 'Bank transfer',
|
|
'check': 'Check payment',
|
|
'lightning': 'Lightning payment',
|
|
'other': 'Payment'
|
|
}
|
|
const methodLabel = paymentMethodLabels[this.settleReceivableDialog.payment_method] || 'Payment'
|
|
const description = this.settleReceivableDialog.description ||
|
|
`${methodLabel} from ${this.settleReceivableDialog.username}`
|
|
|
|
const payload = {
|
|
user_id: this.settleReceivableDialog.user_id,
|
|
amount: this.settleReceivableDialog.amount,
|
|
payment_method: this.settleReceivableDialog.payment_method,
|
|
description: description,
|
|
reference: this.settleReceivableDialog.reference || null,
|
|
}
|
|
|
|
// Add currency info for fiat payments
|
|
if (isCashPayment && this.settleReceivableDialog.fiatCurrency) {
|
|
payload.currency = this.settleReceivableDialog.fiatCurrency
|
|
payload.amount_sats = this.settleReceivableDialog.maxAmount
|
|
}
|
|
|
|
const response = await LNbits.api.request(
|
|
'POST',
|
|
'/castle/api/v1/receivables/settle',
|
|
this.g.user.wallets[0].adminkey,
|
|
payload
|
|
)
|
|
|
|
this.$q.notify({
|
|
type: 'positive',
|
|
message: response.data.message,
|
|
timeout: 3000
|
|
})
|
|
|
|
this.settleReceivableDialog.show = false
|
|
|
|
// Reload balances
|
|
await this.loadBalance()
|
|
await this.loadTransactions()
|
|
await this.loadAllUserBalances()
|
|
} catch (error) {
|
|
LNbits.utils.notifyApiError(error)
|
|
} finally {
|
|
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 : this.currentExchangeRate,
|
|
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
|
|
}
|
|
},
|
|
async loadExchangeRate() {
|
|
try {
|
|
// Fetch current BTC/EUR rate from CoinGecko API
|
|
const response = await fetch('https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=eur')
|
|
const data = await response.json()
|
|
|
|
if (data && data.bitcoin && data.bitcoin.eur) {
|
|
// Convert BTC/EUR to sats/EUR (1 BTC = 100,000,000 sats)
|
|
const btcPerEur = 1 / data.bitcoin.eur
|
|
this.currentExchangeRate = Math.round(btcPerEur * 100000000)
|
|
console.log('Exchange rate loaded:', this.currentExchangeRate, 'sats per EUR')
|
|
} else {
|
|
throw new Error('Invalid response from exchange rate API')
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading exchange rate:', error)
|
|
this.currentExchangeRate = null
|
|
this.$q.notify({
|
|
type: 'warning',
|
|
message: 'Failed to load exchange rate. Fiat currency conversions may not be available.',
|
|
timeout: 5000
|
|
})
|
|
}
|
|
},
|
|
formatSats(amount) {
|
|
return new Intl.NumberFormat().format(amount)
|
|
},
|
|
formatFiat(amount, currency) {
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: currency,
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2
|
|
}).format(amount)
|
|
},
|
|
formatDate(dateString) {
|
|
return new Date(dateString).toLocaleDateString()
|
|
},
|
|
getTotalAmount(entry) {
|
|
if (!entry.lines || entry.lines.length === 0) return 0
|
|
return entry.lines.reduce((sum, line) => sum + line.debit + line.credit, 0) / 2
|
|
},
|
|
getEntryFiatAmount(entry) {
|
|
// Extract fiat amount from metadata if available
|
|
if (!entry.lines || entry.lines.length === 0) return null
|
|
|
|
for (const line of entry.lines) {
|
|
if (line.metadata && line.metadata.fiat_currency && line.metadata.fiat_amount) {
|
|
return this.formatFiat(line.metadata.fiat_amount, line.metadata.fiat_currency)
|
|
}
|
|
}
|
|
return null
|
|
},
|
|
isReceivable(entry) {
|
|
// Check if this is a receivable entry (user owes castle)
|
|
// Receivables have a debit to an "Accounts Receivable" account with the user's ID
|
|
if (!entry.lines || entry.lines.length === 0) return false
|
|
|
|
for (const line of entry.lines) {
|
|
// Look for a line with positive debit on an accounts receivable account
|
|
if (line.debit > 0) {
|
|
// Check if the account is associated with this user's receivables
|
|
const account = this.accounts.find(a => a.id === line.account_id)
|
|
if (account && account.name && account.name.includes('Assets:Receivable') && account.account_type === 'asset') {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
},
|
|
isPayable(entry) {
|
|
// Check if this is a payable entry (castle owes user)
|
|
// Payables have a credit to an "Accounts Payable" account with the user's ID
|
|
if (!entry.lines || entry.lines.length === 0) return false
|
|
|
|
for (const line of entry.lines) {
|
|
// Look for a line with positive credit on an accounts payable account
|
|
if (line.credit > 0) {
|
|
// Check if the account is associated with this user's payables
|
|
const account = this.accounts.find(a => a.id === line.account_id)
|
|
if (account && account.name && account.name.includes('Liabilities:Payable') && account.account_type === 'liability') {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
},
|
|
async created() {
|
|
// Load settings first to determine if user is super user
|
|
await this.loadSettings()
|
|
await this.loadUserWallet()
|
|
await this.loadExchangeRate()
|
|
await this.loadBalance()
|
|
await this.loadTransactions()
|
|
await this.loadAccounts()
|
|
await this.loadCurrencies()
|
|
await this.loadManualPaymentRequests()
|
|
// Load users and pending expenses if super user
|
|
if (this.isSuperUser) {
|
|
await this.loadUsers()
|
|
await this.loadPendingExpenses()
|
|
await this.loadBalanceAssertions()
|
|
await this.loadReconciliationSummary()
|
|
await this.loadReconciliationDiscrepancies()
|
|
}
|
|
}
|
|
})
|