diff --git a/models.py b/models.py index abd508f..2d6b861 100644 --- a/models.py +++ b/models.py @@ -186,10 +186,12 @@ class SettleReceivable(BaseModel): """Manually settle a receivable (user pays castle in person)""" user_id: str - amount: int # Amount in satoshis + amount: Decimal # Amount in the specified currency (or satoshis if currency is None) payment_method: str # "cash", "bank_transfer", "lightning", "other" description: str # Description of the payment reference: Optional[str] = None # Optional reference (receipt number, transaction ID, etc.) + currency: Optional[str] = None # If None, amount is in satoshis. Otherwise, fiat currency code (EUR, USD, etc.) + amount_sats: Optional[int] = None # Equivalent amount in sats (for reference/conversion tracking) class AssertionStatus(str, Enum): diff --git a/static/js/index.js b/static/js/index.js index 82272a0..2d76093 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -96,7 +96,9 @@ window.app = Vue.createApp({ invoice: null, paymentHash: null, checkWalletKey: null, - pollIntervalId: null + pollIntervalId: null, + exchangeRate: 3571.43, // sats per EUR (TODO: fetch from API) + originalCurrency: 'BTC' // Track original receivable currency } } }, @@ -114,6 +116,25 @@ window.app = Vue.createApp({ 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 + } } }, computed: { @@ -135,6 +156,30 @@ window.app = Vue.createApp({ } 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' + }, currencyOptions() { const options = [{label: 'Satoshis (default)', value: null}] this.currencies.forEach(curr => { @@ -883,20 +928,29 @@ window.app = Vue.createApp({ 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), // Convert negative to positive - amount: Math.abs(userBalance.balance), // Default to full amount - payment_method: 'lightning', + 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: `Payment from ${userBalance.username}`, reference: '', loading: false, invoice: null, paymentHash: null, checkWalletKey: null, - pollIntervalId: null + pollIntervalId: null, + exchangeRate: fiatAmount > 0 ? Math.abs(userBalance.balance) / fiatAmount : 3571.43, // Calculate rate from actual amounts + originalCurrency: fiatCurrency || 'BTC' } }, async generateSettlementInvoice() { @@ -1018,17 +1072,30 @@ window.app = Vue.createApp({ 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 + ) + + const payload = { + user_id: this.settleReceivableDialog.user_id, + amount: this.settleReceivableDialog.amount, + payment_method: this.settleReceivableDialog.payment_method, + description: this.settleReceivableDialog.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, - { - user_id: this.settleReceivableDialog.user_id, - amount: this.settleReceivableDialog.amount, - payment_method: this.settleReceivableDialog.payment_method, - description: this.settleReceivableDialog.description, - reference: this.settleReceivableDialog.reference || null - } + payload ) this.$q.notify({ diff --git a/templates/castle/index.html b/templates/castle/index.html index c75bcec..a198d25 100644 --- a/templates/castle/index.html +++ b/templates/castle/index.html @@ -1138,7 +1138,12 @@