Add Lamassu transaction endpoints and UI integration: implement API endpoints for retrieving all processed Lamassu transactions and specific transaction details, including distribution information. Enhance frontend to display transaction data in a table format with export functionality and detailed views for distributions, improving user experience and data accessibility.

This commit is contained in:
padreug 2025-06-20 01:37:31 +02:00
parent dc35cae44e
commit 1af15b6e26
3 changed files with 285 additions and 2 deletions

View file

@ -7,6 +7,7 @@ window.app = Vue.createApp({
// DCA Admin Data // DCA Admin Data
dcaClients: [], dcaClients: [],
deposits: [], deposits: [],
lamassuTransactions: [],
// Table configurations // Table configurations
clientsTable: { clientsTable: {
@ -36,6 +37,30 @@ window.app = Vue.createApp({
rowsPerPage: 10 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 // Dialog states
depositFormDialog: { depositFormDialog: {
@ -49,6 +74,11 @@ window.app = Vue.createApp({
data: null, data: null,
balance: null balance: null
}, },
distributionDialog: {
show: false,
transaction: null,
distributions: []
},
// Quick deposit form // Quick deposit form
quickDepositForm: { quickDepositForm: {
@ -144,6 +174,11 @@ window.app = Vue.createApp({
return date.toLocaleDateString() + ' ' + date.toLocaleTimeString('en-US', { hour12: false }) 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) { getClientUsername(clientId) {
const client = this.dcaClients.find(c => c.id === clientId) const client = this.dcaClients.find(c => c.id === clientId)
return client ? (client.username || client.user_id.substring(0, 8) + '...') : clientId return client ? (client.username || client.user_id.substring(0, 8) + '...') : clientId
@ -468,6 +503,10 @@ window.app = Vue.createApp({
async exportDepositsCSV() { async exportDepositsCSV() {
await LNbits.utils.exportCSV(this.depositsTable.columns, this.deposits) await LNbits.utils.exportCSV(this.depositsTable.columns, this.deposits)
}, },
async exportLamassuTransactionsCSV() {
await LNbits.utils.exportCSV(this.lamassuTransactionsTable.columns, this.lamassuTransactions)
},
// Polling Methods // Polling Methods
async testDatabaseConnection() { async testDatabaseConnection() {
@ -535,6 +574,7 @@ window.app = Vue.createApp({
// Refresh data // Refresh data
await this.getDcaClients() // Refresh to show updated balances await this.getDcaClients() // Refresh to show updated balances
await this.getDeposits() await this.getDeposits()
await this.getLamassuTransactions()
await this.getLamassuConfig() await this.getLamassuConfig()
} catch (error) { } catch (error) {
LNbits.utils.notifyApiError(error) LNbits.utils.notifyApiError(error)
@ -586,6 +626,7 @@ window.app = Vue.createApp({
// Refresh data // Refresh data
await this.getDcaClients() // Refresh to show updated balances await this.getDcaClients() // Refresh to show updated balances
await this.getDeposits() await this.getDeposits()
await this.getLamassuTransactions()
await this.getLamassuConfig() await this.getLamassuConfig()
} catch (error) { } 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) // Legacy Methods (keep for backward compatibility)
async closeFormDialog() { async closeFormDialog() {
this.formDialog.show = false this.formDialog.show = false
@ -792,7 +863,8 @@ window.app = Vue.createApp({
await Promise.all([ await Promise.all([
this.getLamassuConfig(), this.getLamassuConfig(),
this.getDcaClients(), this.getDcaClients(),
this.getDeposits() this.getDeposits(),
this.getLamassuTransactions()
]) ])
// Legacy data loading // Legacy data loading

View file

@ -213,6 +213,58 @@
</q-card-section> </q-card-section>
</q-card> </q-card>
<!-- Lamassu Transactions Section -->
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h6 class="text-subtitle2 q-my-none">Processed Lamassu Transactions</h6>
<p class="text-caption q-my-none">ATM transactions processed through DCA distribution</p>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportLamassuTransactionsCSV">Export to CSV</q-btn>
</div>
</div>
<q-table
dense
flat
:rows="lamassuTransactions"
row-key="id"
:columns="lamassuTransactionsTable.columns"
v-model:pagination="lamassuTransactionsTable.pagination"
>
<template v-slot:body="props">
<q-tr :props="props" class="cursor-pointer" @click="viewTransactionDistributions(props.row)">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
<div v-if="col.field == 'lamassu_transaction_id'">${ col.value }</div>
<div v-else-if="col.field == 'fiat_amount'">${ formatCurrency(col.value) }</div>
<div v-else-if="col.field == 'crypto_amount'">${ formatSats(col.value) }</div>
<div v-else-if="col.field == 'commission_amount_sats'">${ formatSats(col.value) }</div>
<div v-else-if="col.field == 'base_amount_sats'">${ formatSats(col.value) }</div>
<div v-else-if="col.field == 'distributions_total_sats'">${ formatSats(col.value) }</div>
<div v-else-if="col.field == 'commission_percentage'">${ (col.value * 100).toFixed(1) }%</div>
<div v-else-if="col.field == 'effective_commission'">${ (col.value * 100).toFixed(1) }%</div>
<div v-else-if="col.field == 'discount'">${ col.value }%</div>
<div v-else-if="col.field == 'exchange_rate'">${ col.value.toLocaleString() }</div>
<div v-else-if="col.field == 'transaction_time'">${ formatDateTime(col.value) }</div>
<div v-else-if="col.field == 'processed_at'">${ formatDateTime(col.value) }</div>
<div v-else>${ col.value || '-' }</div>
</q-td>
<q-td auto-width>
<q-btn
flat dense size="sm" icon="visibility"
color="primary"
@click.stop="viewTransactionDistributions(props.row)"
>
<q-tooltip>View Distribution Details</q-tooltip>
</q-btn>
</q-td>
</q-tr>
</template>
</q-table>
</q-card-section>
</q-card>
<q-card> <q-card>
<q-card-section> <q-card-section>
<div class="row items-center no-wrap q-mb-md"> <div class="row items-center no-wrap q-mb-md">
@ -707,6 +759,101 @@
</q-card> </q-card>
</q-dialog> </q-dialog>
<!--/////////////////////////////////////////////////-->
<!--//////////////TRANSACTION DISTRIBUTIONS DIALOG////-->
<!--/////////////////////////////////////////////////-->
<q-dialog v-model="distributionDialog.show" position="top" maximized>
<q-card class="q-pa-lg">
<div class="text-h6 q-mb-md">Transaction Distribution Details</div>
<div v-if="distributionDialog.transaction" class="q-mb-lg">
<q-list>
<q-item>
<q-item-section>
<q-item-label caption>Lamassu Transaction ID</q-item-label>
<q-item-label>${ distributionDialog.transaction.lamassu_transaction_id }</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>Transaction Time</q-item-label>
<q-item-label>${ formatDateTime(distributionDialog.transaction.transaction_time) }</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>Total Amount</q-item-label>
<q-item-label>
${ formatCurrency(distributionDialog.transaction.fiat_amount) }
(${ formatSats(distributionDialog.transaction.crypto_amount) })
</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>Commission</q-item-label>
<q-item-label>
${ (distributionDialog.transaction.commission_percentage * 100).toFixed(1) }%
<span v-if="distributionDialog.transaction.discount > 0">
(with ${ distributionDialog.transaction.discount }% discount = ${ (distributionDialog.transaction.effective_commission * 100).toFixed(1) }% effective)
</span>
= ${ formatSats(distributionDialog.transaction.commission_amount_sats) }
</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>Available for Distribution</q-item-label>
<q-item-label>${ formatSats(distributionDialog.transaction.base_amount_sats) }</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>Total Distributed</q-item-label>
<q-item-label>${ formatSats(distributionDialog.transaction.distributions_total_sats) } to ${ distributionDialog.transaction.clients_count } clients</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<q-separator class="q-my-md"></q-separator>
<div class="text-h6 q-mb-md">Client Distributions</div>
<q-table
dense
flat
:rows="distributionDialog.distributions"
row-key="payment_id"
:columns="distributionDetailsTable.columns"
>
<template v-slot:body="props">
<q-tr :props="props">
<q-td v-for="col in props.cols" :key="col.name" :props="props">
<div v-if="col.field == 'client_username'">${ col.value || 'No username' }</div>
<div v-else-if="col.field == 'client_user_id'">${ col.value.substring(0, 8) }...</div>
<div v-else-if="col.field == 'amount_sats'">${ formatSats(col.value) }</div>
<div v-else-if="col.field == 'amount_fiat'">${ formatCurrency(col.value) }</div>
<div v-else-if="col.field == 'exchange_rate'">${ col.value.toLocaleString() }</div>
<div v-else-if="col.field == 'status'">
<q-badge :color="col.value === 'confirmed' ? 'green' : col.value === 'failed' ? 'red' : 'orange'">
${ col.value }
</q-badge>
</div>
<div v-else-if="col.field == 'created_at'">${ formatDateTime(col.value) }</div>
<div v-else>${ col.value || '-' }</div>
</q-td>
</q-tr>
</template>
</q-table>
<div class="row q-mt-lg">
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div>
</q-card>
</q-dialog>
<!--/////////////////////////////////////////////////--> <!--/////////////////////////////////////////////////-->
<!--//////////////QR Code DIALOG/////////////////////--> <!--//////////////QR Code DIALOG/////////////////////-->
<!--/////////////////////////////////////////////////--> <!--/////////////////////////////////////////////////-->

View file

@ -35,6 +35,9 @@ from .crud import (
update_lamassu_config, update_lamassu_config,
update_config_test_result, update_config_test_result,
delete_lamassu_config, delete_lamassu_config,
# Lamassu transaction CRUD operations
get_all_lamassu_transactions,
get_lamassu_transaction
) )
from .helpers import lnurler from .helpers import lnurler
from .models import ( from .models import (
@ -43,7 +46,8 @@ from .models import (
CreateDcaClientData, DcaClient, UpdateDcaClientData, CreateDcaClientData, DcaClient, UpdateDcaClientData,
CreateDepositData, DcaDeposit, UpdateDepositStatusData, CreateDepositData, DcaDeposit, UpdateDepositStatusData,
ClientBalanceSummary, ClientBalanceSummary,
CreateLamassuConfigData, LamassuConfig, UpdateLamassuConfigData CreateLamassuConfigData, LamassuConfig, UpdateLamassuConfigData,
StoredLamassuTransaction
) )
myextension_api_router = APIRouter() 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 # Lamassu Configuration Endpoints
@myextension_api_router.get("/api/v1/dca/config") @myextension_api_router.get("/api/v1/dca/config")