diff --git a/static/js/index.js b/static/js/index.js index 8d48a09..67ae246 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -7,6 +7,7 @@ window.app = Vue.createApp({ // DCA Admin Data dcaClients: [], deposits: [], + lamassuTransactions: [], // Table configurations clientsTable: { @@ -36,6 +37,30 @@ window.app = Vue.createApp({ rowsPerPage: 10 } }, + lamassuTransactionsTable: { + columns: [ + { name: 'lamassu_transaction_id', align: 'left', label: 'Transaction ID', field: 'lamassu_transaction_id' }, + { name: 'transaction_time', align: 'left', label: 'Time', field: 'transaction_time' }, + { name: 'fiat_amount', align: 'right', label: 'Fiat Amount', field: 'fiat_amount' }, + { name: 'crypto_amount', align: 'right', label: 'Total Sats', field: 'crypto_amount' }, + { name: 'commission_amount_sats', align: 'right', label: 'Commission', field: 'commission_amount_sats' }, + { name: 'base_amount_sats', align: 'right', label: 'Base Amount', field: 'base_amount_sats' }, + { name: 'distributions_total_sats', align: 'right', label: 'Distributed', field: 'distributions_total_sats' }, + { name: 'clients_count', align: 'center', label: 'Clients', field: 'clients_count' } + ], + pagination: { + rowsPerPage: 10 + } + }, + distributionDetailsTable: { + columns: [ + { name: 'client_username', align: 'left', label: 'Client', field: 'client_username' }, + { name: 'amount_sats', align: 'right', label: 'Amount (sats)', field: 'amount_sats' }, + { name: 'amount_fiat', align: 'right', label: 'Amount (fiat)', field: 'amount_fiat' }, + { name: 'status', align: 'center', label: 'Status', field: 'status' }, + { name: 'created_at', align: 'left', label: 'Created', field: 'created_at' } + ] + }, // Dialog states depositFormDialog: { @@ -49,6 +74,11 @@ window.app = Vue.createApp({ data: null, balance: null }, + distributionDialog: { + show: false, + transaction: null, + distributions: [] + }, // Quick deposit form quickDepositForm: { @@ -144,6 +174,11 @@ window.app = Vue.createApp({ return date.toLocaleDateString() + ' ' + date.toLocaleTimeString('en-US', { hour12: false }) }, + formatSats(amount) { + if (!amount) return '0 sats' + return new Intl.NumberFormat('en-US').format(amount) + ' sats' + }, + getClientUsername(clientId) { const client = this.dcaClients.find(c => c.id === clientId) return client ? (client.username || client.user_id.substring(0, 8) + '...') : clientId @@ -468,6 +503,10 @@ window.app = Vue.createApp({ async exportDepositsCSV() { await LNbits.utils.exportCSV(this.depositsTable.columns, this.deposits) }, + + async exportLamassuTransactionsCSV() { + await LNbits.utils.exportCSV(this.lamassuTransactionsTable.columns, this.lamassuTransactions) + }, // Polling Methods async testDatabaseConnection() { @@ -535,6 +574,7 @@ window.app = Vue.createApp({ // Refresh data await this.getDcaClients() // Refresh to show updated balances await this.getDeposits() + await this.getLamassuTransactions() await this.getLamassuConfig() } catch (error) { LNbits.utils.notifyApiError(error) @@ -586,6 +626,7 @@ window.app = Vue.createApp({ // Refresh data await this.getDcaClients() // Refresh to show updated balances await this.getDeposits() + await this.getLamassuTransactions() await this.getLamassuConfig() } catch (error) { @@ -595,6 +636,36 @@ window.app = Vue.createApp({ } }, + // Lamassu Transaction Methods + async getLamassuTransactions() { + try { + const { data } = await LNbits.api.request( + 'GET', + '/myextension/api/v1/dca/transactions', + this.g.user.wallets[0].inkey + ) + this.lamassuTransactions = data + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + + async viewTransactionDistributions(transaction) { + try { + const { data: distributions } = await LNbits.api.request( + 'GET', + `/myextension/api/v1/dca/transactions/${transaction.id}/distributions`, + this.g.user.wallets[0].inkey + ) + + this.distributionDialog.transaction = transaction + this.distributionDialog.distributions = distributions + this.distributionDialog.show = true + } catch (error) { + LNbits.utils.notifyApiError(error) + } + }, + // Legacy Methods (keep for backward compatibility) async closeFormDialog() { this.formDialog.show = false @@ -792,7 +863,8 @@ window.app = Vue.createApp({ await Promise.all([ this.getLamassuConfig(), this.getDcaClients(), - this.getDeposits() + this.getDeposits(), + this.getLamassuTransactions() ]) // Legacy data loading diff --git a/templates/myextension/index.html b/templates/myextension/index.html index 95ccbf3..fc455c2 100644 --- a/templates/myextension/index.html +++ b/templates/myextension/index.html @@ -213,6 +213,58 @@ + + + +
+
+
Processed Lamassu Transactions
+

ATM transactions processed through DCA distribution

+
+
+ Export to CSV +
+
+ + + +
+
+
@@ -707,6 +759,101 @@ + + + + + + +
Transaction Distribution Details
+ +
+ + + + Lamassu Transaction ID + ${ distributionDialog.transaction.lamassu_transaction_id } + + + + + Transaction Time + ${ formatDateTime(distributionDialog.transaction.transaction_time) } + + + + + Total Amount + + ${ formatCurrency(distributionDialog.transaction.fiat_amount) } + (${ formatSats(distributionDialog.transaction.crypto_amount) }) + + + + + + Commission + + ${ (distributionDialog.transaction.commission_percentage * 100).toFixed(1) }% + + (with ${ distributionDialog.transaction.discount }% discount = ${ (distributionDialog.transaction.effective_commission * 100).toFixed(1) }% effective) + + = ${ formatSats(distributionDialog.transaction.commission_amount_sats) } + + + + + + Available for Distribution + ${ formatSats(distributionDialog.transaction.base_amount_sats) } + + + + + Total Distributed + ${ formatSats(distributionDialog.transaction.distributions_total_sats) } to ${ distributionDialog.transaction.clients_count } clients + + + +
+ + + +
Client Distributions
+ + + + + +
+ Close +
+
+
+ diff --git a/views_api.py b/views_api.py index b471db1..a8b2fd0 100644 --- a/views_api.py +++ b/views_api.py @@ -35,6 +35,9 @@ from .crud import ( update_lamassu_config, update_config_test_result, delete_lamassu_config, + # Lamassu transaction CRUD operations + get_all_lamassu_transactions, + get_lamassu_transaction ) from .helpers import lnurler from .models import ( @@ -43,7 +46,8 @@ from .models import ( CreateDcaClientData, DcaClient, UpdateDcaClientData, CreateDepositData, DcaDeposit, UpdateDepositStatusData, ClientBalanceSummary, - CreateLamassuConfigData, LamassuConfig, UpdateLamassuConfigData + CreateLamassuConfigData, LamassuConfig, UpdateLamassuConfigData, + StoredLamassuTransaction ) myextension_api_router = APIRouter() @@ -450,6 +454,66 @@ async def api_test_transaction( ) +# Lamassu Transaction Endpoints + +@myextension_api_router.get("/api/v1/dca/transactions") +async def api_get_lamassu_transactions( + wallet: WalletTypeInfo = Depends(require_invoice_key), +) -> list[StoredLamassuTransaction]: + """Get all processed Lamassu transactions""" + return await get_all_lamassu_transactions() + + +@myextension_api_router.get("/api/v1/dca/transactions/{transaction_id}") +async def api_get_lamassu_transaction( + transaction_id: str, + wallet: WalletTypeInfo = Depends(require_invoice_key), +) -> StoredLamassuTransaction: + """Get a specific Lamassu transaction with details""" + transaction = await get_lamassu_transaction(transaction_id) + if not transaction: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Lamassu transaction not found." + ) + return transaction + + +@myextension_api_router.get("/api/v1/dca/transactions/{transaction_id}/distributions") +async def api_get_transaction_distributions( + transaction_id: str, + wallet: WalletTypeInfo = Depends(require_invoice_key), +) -> list[dict]: + """Get distribution details for a specific Lamassu transaction""" + # Get the stored transaction + transaction = await get_lamassu_transaction(transaction_id) + if not transaction: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Lamassu transaction not found." + ) + + # Get all DCA payments for this Lamassu transaction + from .crud import get_payments_by_lamassu_transaction, get_dca_client + payments = await get_payments_by_lamassu_transaction(transaction.lamassu_transaction_id) + + # Enhance payments with client information + distributions = [] + for payment in payments: + client = await get_dca_client(payment.client_id) + distributions.append({ + "payment_id": payment.id, + "client_id": payment.client_id, + "client_username": client.username if client else None, + "client_user_id": client.user_id if client else None, + "amount_sats": payment.amount_sats, + "amount_fiat": payment.amount_fiat, + "exchange_rate": payment.exchange_rate, + "status": payment.status, + "created_at": payment.created_at + }) + + return distributions + + # Lamassu Configuration Endpoints @myextension_api_router.get("/api/v1/dca/config")