Adds a date field to expense entries for better tracking and reporting. This allows users to specify the date of the expense transaction, providing more accurate financial records.
1471 lines
47 KiB
JavaScript
1471 lines
47 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
|
|
},
|
|
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()
|
|
}
|
|
}
|
|
})
|