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
|
// 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
|
||||||
|
|
|
||||||
|
|
@ -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/////////////////////-->
|
||||||
<!--/////////////////////////////////////////////////-->
|
<!--/////////////////////////////////////////////////-->
|
||||||
|
|
|
||||||
66
views_api.py
66
views_api.py
|
|
@ -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")
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue