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:
parent
dc35cae44e
commit
1af15b6e26
3 changed files with 285 additions and 2 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -213,6 +213,58 @@
|
|||
</q-card-section>
|
||||
</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-section>
|
||||
<div class="row items-center no-wrap q-mb-md">
|
||||
|
|
@ -707,6 +759,101 @@
|
|||
</q-card>
|
||||
</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/////////////////////-->
|
||||
<!--/////////////////////////////////////////////////-->
|
||||
|
|
|
|||
66
views_api.py
66
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")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue