Adds manual transaction processing feature
Implements functionality to manually process specific Lamassu transactions by ID, bypassing dispense checks. This allows administrators to handle transactions that may have failed due to dispense issues or were settled manually outside of the automated process. The feature includes a new UI dialog for entering the transaction ID and an API endpoint to fetch and process the transaction, crediting wallets and distributing funds according to the DCA configuration.
This commit is contained in:
parent
bd7a72f3c0
commit
9c2a1f7d4a
4 changed files with 272 additions and 17 deletions
|
|
@ -92,8 +92,15 @@ window.app = Vue.createApp({
|
|||
testingConnection: false,
|
||||
runningManualPoll: false,
|
||||
runningTestTransaction: false,
|
||||
processingSpecificTransaction: false,
|
||||
lamassuConfig: null,
|
||||
|
||||
// Manual transaction processing
|
||||
manualTransactionDialog: {
|
||||
show: false,
|
||||
transactionId: ''
|
||||
},
|
||||
|
||||
// Config dialog
|
||||
configDialog: {
|
||||
show: false,
|
||||
|
|
@ -586,6 +593,79 @@ window.app = Vue.createApp({
|
|||
await this.getDeposits()
|
||||
await this.getLamassuTransactions()
|
||||
await this.getLamassuConfig()
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
} finally {
|
||||
this.runningTestTransaction = false
|
||||
}
|
||||
},
|
||||
|
||||
openManualTransactionDialog() {
|
||||
this.manualTransactionDialog.transactionId = ''
|
||||
this.manualTransactionDialog.show = true
|
||||
},
|
||||
|
||||
async processSpecificTransaction() {
|
||||
if (!this.manualTransactionDialog.transactionId) {
|
||||
this.$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Please enter a transaction ID',
|
||||
timeout: 3000
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.processingSpecificTransaction = true
|
||||
try {
|
||||
const { data } = await LNbits.api.request(
|
||||
'POST',
|
||||
`/satmachineadmin/api/v1/dca/process-transaction/${this.manualTransactionDialog.transactionId}`,
|
||||
null
|
||||
)
|
||||
|
||||
if (data.already_processed) {
|
||||
this.$q.notify({
|
||||
type: 'warning',
|
||||
message: `Transaction already processed with ${data.payment_count} distributions`,
|
||||
timeout: 5000
|
||||
})
|
||||
this.manualTransactionDialog.show = false
|
||||
return
|
||||
}
|
||||
|
||||
// Show detailed results
|
||||
const details = data.transaction_details
|
||||
let dialogContent = `<strong>Manual Transaction Processing Results</strong><br/><br/>`
|
||||
dialogContent += `<strong>Transaction ID:</strong> ${details.transaction_id}<br/>`
|
||||
dialogContent += `<strong>Status:</strong> ${details.status}<br/>`
|
||||
dialogContent += `<strong>Dispense:</strong> ${details.dispense ? 'Yes' : 'No'}<br/>`
|
||||
dialogContent += `<strong>Dispense Confirmed:</strong> ${details.dispense_confirmed ? 'Yes' : 'No'}<br/>`
|
||||
dialogContent += `<strong>Crypto Amount:</strong> ${details.crypto_amount} sats<br/>`
|
||||
dialogContent += `<strong>Fiat Amount:</strong> ${details.fiat_amount}<br/>`
|
||||
dialogContent += `<br/><strong>Transaction processed successfully!</strong>`
|
||||
|
||||
this.$q.dialog({
|
||||
title: 'Transaction Processed',
|
||||
message: dialogContent,
|
||||
html: true,
|
||||
ok: {
|
||||
color: 'positive',
|
||||
label: 'Great!'
|
||||
}
|
||||
})
|
||||
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: `Transaction ${details.transaction_id} processed successfully`,
|
||||
timeout: 5000
|
||||
})
|
||||
|
||||
// Close dialog and refresh data
|
||||
this.manualTransactionDialog.show = false
|
||||
await this.getDcaClients()
|
||||
await this.getDeposits()
|
||||
await this.getLamassuTransactions()
|
||||
await this.getLamassuConfig()
|
||||
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
|
|
|
|||
|
|
@ -335,21 +335,32 @@
|
|||
>
|
||||
Test Connection
|
||||
</q-btn>
|
||||
<q-btn
|
||||
<q-btn
|
||||
v-if="lamassuConfig"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
@click="manualPoll"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
@click="manualPoll"
|
||||
:loading="runningManualPoll"
|
||||
class="q-ml-sm"
|
||||
>
|
||||
Manual Poll
|
||||
</q-btn>
|
||||
<q-btn
|
||||
<q-btn
|
||||
v-if="lamassuConfig"
|
||||
size="sm"
|
||||
color="deep-orange"
|
||||
@click="openManualTransactionDialog"
|
||||
class="q-ml-sm"
|
||||
icon="build"
|
||||
>
|
||||
<q-tooltip>Process specific transaction by ID (bypasses dispense checks)</q-tooltip>
|
||||
Manual TX
|
||||
</q-btn>
|
||||
<q-btn
|
||||
v-if="lamassuConfig && lamassuConfig.source_wallet_id"
|
||||
size="sm"
|
||||
color="warning"
|
||||
@click="testTransaction"
|
||||
size="sm"
|
||||
color="warning"
|
||||
@click="testTransaction"
|
||||
:loading="runningTestTransaction"
|
||||
class="q-ml-sm"
|
||||
>
|
||||
|
|
@ -690,7 +701,7 @@
|
|||
<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>
|
||||
|
|
@ -709,7 +720,7 @@
|
|||
<q-item-section>
|
||||
<q-item-label caption>Total Amount</q-item-label>
|
||||
<q-item-label>
|
||||
${ formatCurrency(distributionDialog.transaction.fiat_amount) }
|
||||
${ formatCurrency(distributionDialog.transaction.fiat_amount) }
|
||||
(${ formatSats(distributionDialog.transaction.crypto_amount) })
|
||||
</q-item-label>
|
||||
</q-item-section>
|
||||
|
|
@ -718,7 +729,7 @@
|
|||
<q-item-section>
|
||||
<q-item-label caption>Commission</q-item-label>
|
||||
<q-item-label>
|
||||
${ (distributionDialog.transaction.commission_percentage * 100).toFixed(1) }%
|
||||
${ (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>
|
||||
|
|
@ -740,11 +751,11 @@
|
|||
</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
|
||||
|
|
@ -771,12 +782,71 @@
|
|||
</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>
|
||||
|
||||
<!--/////////////////////////////////////////////////-->
|
||||
<!--//////////////MANUAL TRANSACTION DIALOG///////////-->
|
||||
<!--/////////////////////////////////////////////////-->
|
||||
|
||||
<q-dialog v-model="manualTransactionDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px; max-width: 90vw">
|
||||
<div class="text-h6 q-mb-md">Process Specific Transaction</div>
|
||||
|
||||
<q-banner class="bg-orange-1 text-orange-9 q-mb-md">
|
||||
<template v-slot:avatar>
|
||||
<q-icon name="warning" color="orange" />
|
||||
</template>
|
||||
<div class="text-caption">
|
||||
<strong>Use with caution:</strong> This bypasses all dispense status checks and will process the transaction even if dispense_confirmed is false. Only use this for manually settled transactions.
|
||||
</div>
|
||||
</q-banner>
|
||||
|
||||
<q-form @submit="processSpecificTransaction" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="manualTransactionDialog.transactionId"
|
||||
label="Lamassu Transaction ID *"
|
||||
placeholder="e.g., 82746dfb-674d-4d7e-b008-7507caa02773"
|
||||
hint="Enter the transaction/session ID from Lamassu database"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="receipt" />
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div class="text-caption text-grey-7">
|
||||
This will:
|
||||
<ul class="q-my-sm">
|
||||
<li>Fetch the transaction from Lamassu regardless of dispense status</li>
|
||||
<li>Process it through the normal DCA distribution flow</li>
|
||||
<li>Credit the source wallet and distribute to clients</li>
|
||||
<li>Send commission to the commission wallet (if configured)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-orange"
|
||||
type="submit"
|
||||
:loading="processingSpecificTransaction"
|
||||
:disable="!manualTransactionDialog.transactionId"
|
||||
>
|
||||
Process Transaction
|
||||
</q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">
|
||||
Cancel
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -560,6 +560,44 @@ class LamassuTransactionProcessor:
|
|||
logger.error(f"Error executing SSH query: {e}")
|
||||
return []
|
||||
|
||||
async def fetch_transaction_by_id(self, db_config: Dict[str, Any], transaction_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Fetch a specific transaction by ID from Lamassu database, bypassing all status filters"""
|
||||
try:
|
||||
logger.info(f"Fetching transaction {transaction_id} from Lamassu database (bypass all filters)")
|
||||
|
||||
# Query for specific transaction ID without any status/dispense filters
|
||||
lamassu_query = f"""
|
||||
SELECT
|
||||
co.id as transaction_id,
|
||||
co.fiat as fiat_amount,
|
||||
co.crypto_atoms as crypto_amount,
|
||||
co.confirmed_at as transaction_time,
|
||||
co.device_id,
|
||||
co.status,
|
||||
co.commission_percentage,
|
||||
co.discount,
|
||||
co.crypto_code,
|
||||
co.fiat_code,
|
||||
co.dispense,
|
||||
co.dispense_confirmed
|
||||
FROM cash_out_txs co
|
||||
WHERE co.id = '{transaction_id}'
|
||||
"""
|
||||
|
||||
results = await self.execute_ssh_query(db_config, lamassu_query)
|
||||
|
||||
if not results:
|
||||
logger.warning(f"Transaction {transaction_id} not found in Lamassu database")
|
||||
return None
|
||||
|
||||
transaction = results[0]
|
||||
logger.info(f"Found transaction {transaction_id}: status={transaction.get('status')}, dispense={transaction.get('dispense')}, dispense_confirmed={transaction.get('dispense_confirmed')}")
|
||||
return transaction
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching transaction {transaction_id} from Lamassu database: {e}")
|
||||
return None
|
||||
|
||||
async def fetch_new_transactions(self, db_config: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""Fetch new successful transactions from Lamassu database since last poll"""
|
||||
try:
|
||||
|
|
@ -573,13 +611,13 @@ class LamassuTransactionProcessor:
|
|||
# Fallback to last 24 hours for first run or if no previous poll
|
||||
time_threshold = datetime.now(timezone.utc) - timedelta(hours=24)
|
||||
logger.info(f"No previous poll found, checking last 24 hours since: {time_threshold}")
|
||||
|
||||
|
||||
# Convert to UTC if not already timezone-aware
|
||||
if time_threshold.tzinfo is None:
|
||||
time_threshold = time_threshold.replace(tzinfo=timezone.utc)
|
||||
elif time_threshold.tzinfo != timezone.utc:
|
||||
time_threshold = time_threshold.astimezone(timezone.utc)
|
||||
|
||||
|
||||
# Format as UTC for database query
|
||||
time_threshold_str = time_threshold.strftime('%Y-%m-%d %H:%M:%S UTC')
|
||||
|
||||
|
|
|
|||
67
views_api.py
67
views_api.py
|
|
@ -233,6 +233,73 @@ async def api_manual_poll(
|
|||
)
|
||||
|
||||
|
||||
@satmachineadmin_api_router.post("/api/v1/dca/process-transaction/{transaction_id}")
|
||||
async def api_process_specific_transaction(
|
||||
transaction_id: str,
|
||||
user: User = Depends(check_super_user),
|
||||
):
|
||||
"""
|
||||
Manually process a specific Lamassu transaction by ID, bypassing all status filters.
|
||||
|
||||
This endpoint is useful for processing transactions that were manually settled
|
||||
or had dispense issues but need to be included in DCA distribution.
|
||||
"""
|
||||
try:
|
||||
from .transaction_processor import transaction_processor
|
||||
from .crud import get_payments_by_lamassu_transaction
|
||||
|
||||
# Get database configuration
|
||||
db_config = await transaction_processor.connect_to_lamassu_db()
|
||||
if not db_config:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.SERVICE_UNAVAILABLE,
|
||||
detail="Could not get Lamassu database configuration",
|
||||
)
|
||||
|
||||
# Check if transaction was already processed
|
||||
existing_payments = await get_payments_by_lamassu_transaction(transaction_id)
|
||||
if existing_payments:
|
||||
return {
|
||||
"success": False,
|
||||
"already_processed": True,
|
||||
"message": f"Transaction {transaction_id} was already processed with {len(existing_payments)} distributions",
|
||||
"payment_count": len(existing_payments),
|
||||
}
|
||||
|
||||
# Fetch the specific transaction from Lamassu (bypassing all filters)
|
||||
transaction = await transaction_processor.fetch_transaction_by_id(db_config, transaction_id)
|
||||
|
||||
if not transaction:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail=f"Transaction {transaction_id} not found in Lamassu database",
|
||||
)
|
||||
|
||||
# Process the transaction through normal DCA flow
|
||||
await transaction_processor.process_transaction(transaction)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Transaction {transaction_id} processed successfully",
|
||||
"transaction_details": {
|
||||
"transaction_id": transaction_id,
|
||||
"status": transaction.get("status"),
|
||||
"dispense": transaction.get("dispense"),
|
||||
"dispense_confirmed": transaction.get("dispense_confirmed"),
|
||||
"crypto_amount": transaction.get("crypto_amount"),
|
||||
"fiat_amount": transaction.get("fiat_amount"),
|
||||
},
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error processing transaction {transaction_id}: {str(e)}",
|
||||
)
|
||||
|
||||
|
||||
@satmachineadmin_api_router.post("/api/v1/dca/test-transaction")
|
||||
async def api_test_transaction(
|
||||
user: User = Depends(check_super_user),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue