Compare commits

...

5 commits
v0.0.1 ... main

Author SHA1 Message Date
cd0d958c2c consolidate docs
Some checks are pending
CI / lint (push) Waiting to run
CI / tests (3.10) (push) Blocked by required conditions
CI / tests (3.9) (push) Blocked by required conditions
/ release (push) Waiting to run
/ pullrequest (push) Blocked by required conditions
2025-11-03 22:23:10 +01:00
1b7374fa70 Removes test transaction UI button
Removes the test transaction button from the admin UI.

The test transaction endpoint is still available in the API for development and debugging purposes.
2025-11-03 22:23:10 +01:00
fe38e08d4e 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.
2025-11-03 22:23:08 +01:00
230beccc37 Improve balance verification in LamassuTransactionProcessor: Added rounding to two decimal places for balance and fiat amounts to ensure precision in comparisons. Enhanced logging for insufficient balance scenarios, improving clarity in transaction processing and error reporting. 2025-11-03 22:23:04 +01:00
077e097fc2 Refactor transaction data handling in LamassuTransactionProcessor: Improved consistency in processing None and empty values during data ingestion. Defaulted numeric fields to 0 and percentage fields to 0.0 for better error handling. Ensured clean extraction of transaction details, enhancing reliability in transaction processing. 2025-11-03 22:23:04 +01:00
7 changed files with 3068 additions and 81 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -92,8 +92,15 @@ window.app = Vue.createApp({
testingConnection: false, testingConnection: false,
runningManualPoll: false, runningManualPoll: false,
runningTestTransaction: false, runningTestTransaction: false,
processingSpecificTransaction: false,
lamassuConfig: null, lamassuConfig: null,
// Manual transaction processing
manualTransactionDialog: {
show: false,
transactionId: ''
},
// Config dialog // Config dialog
configDialog: { configDialog: {
show: false, show: false,
@ -586,6 +593,79 @@ window.app = Vue.createApp({
await this.getDeposits() await this.getDeposits()
await this.getLamassuTransactions() await this.getLamassuTransactions()
await this.getLamassuConfig() 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) { } catch (error) {
LNbits.utils.notifyApiError(error) LNbits.utils.notifyApiError(error)

View file

@ -335,25 +335,26 @@
> >
Test Connection Test Connection
</q-btn> </q-btn>
<q-btn <q-btn
v-if="lamassuConfig" v-if="lamassuConfig"
size="sm" size="sm"
color="secondary" color="secondary"
@click="manualPoll" @click="manualPoll"
:loading="runningManualPoll" :loading="runningManualPoll"
class="q-ml-sm" class="q-ml-sm"
> >
Manual Poll Manual Poll
</q-btn> </q-btn>
<q-btn <q-btn
v-if="lamassuConfig && lamassuConfig.source_wallet_id" v-if="lamassuConfig"
size="sm" size="sm"
color="warning" color="deep-orange"
@click="testTransaction" @click="openManualTransactionDialog"
:loading="runningTestTransaction"
class="q-ml-sm" class="q-ml-sm"
icon="build"
> >
Test Transaction <q-tooltip>Process specific transaction by ID (bypasses dispense checks)</q-tooltip>
Manual TX
</q-btn> </q-btn>
</div> </div>
</q-card-section> </q-card-section>
@ -690,7 +691,7 @@
<q-dialog v-model="distributionDialog.show" position="top" maximized> <q-dialog v-model="distributionDialog.show" position="top" maximized>
<q-card class="q-pa-lg"> <q-card class="q-pa-lg">
<div class="text-h6 q-mb-md">Transaction Distribution Details</div> <div class="text-h6 q-mb-md">Transaction Distribution Details</div>
<div v-if="distributionDialog.transaction" class="q-mb-lg"> <div v-if="distributionDialog.transaction" class="q-mb-lg">
<q-list> <q-list>
<q-item> <q-item>
@ -709,7 +710,7 @@
<q-item-section> <q-item-section>
<q-item-label caption>Total Amount</q-item-label> <q-item-label caption>Total Amount</q-item-label>
<q-item-label> <q-item-label>
${ formatCurrency(distributionDialog.transaction.fiat_amount) } ${ formatCurrency(distributionDialog.transaction.fiat_amount) }
(${ formatSats(distributionDialog.transaction.crypto_amount) }) (${ formatSats(distributionDialog.transaction.crypto_amount) })
</q-item-label> </q-item-label>
</q-item-section> </q-item-section>
@ -718,7 +719,7 @@
<q-item-section> <q-item-section>
<q-item-label caption>Commission</q-item-label> <q-item-label caption>Commission</q-item-label>
<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"> <span v-if="distributionDialog.transaction.discount > 0">
(with ${ distributionDialog.transaction.discount }% discount = ${ (distributionDialog.transaction.effective_commission * 100).toFixed(1) }% effective) (with ${ distributionDialog.transaction.discount }% discount = ${ (distributionDialog.transaction.effective_commission * 100).toFixed(1) }% effective)
</span> </span>
@ -740,11 +741,11 @@
</q-item> </q-item>
</q-list> </q-list>
</div> </div>
<q-separator class="q-my-md"></q-separator> <q-separator class="q-my-md"></q-separator>
<div class="text-h6 q-mb-md">Client Distributions</div> <div class="text-h6 q-mb-md">Client Distributions</div>
<q-table <q-table
dense dense
flat flat
@ -771,12 +772,71 @@
</q-tr> </q-tr>
</template> </template>
</q-table> </q-table>
<div class="row q-mt-lg"> <div class="row q-mt-lg">
<q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn> <q-btn v-close-popup flat color="grey" class="q-ml-auto">Close</q-btn>
</div> </div>
</q-card> </q-card>
</q-dialog> </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> </div>
{% endblock %} {% endblock %}

View file

@ -493,14 +493,26 @@ class LamassuTransactionProcessor:
# Convert string values to appropriate types # Convert string values to appropriate types
processed_row = {} processed_row = {}
for key, value in row.items(): for key, value in row.items():
if value == '': # Handle None/empty values consistently at data ingestion boundary
processed_row[key] = None if value == '' or value is None:
if key in ['fiat_amount', 'crypto_amount']:
processed_row[key] = 0 # Default numeric fields to 0
elif key in ['commission_percentage', 'discount']:
processed_row[key] = 0.0 # Default percentage fields to 0.0
else:
processed_row[key] = None # Keep None for non-numeric fields
elif key in ['transaction_id', 'device_id', 'crypto_code', 'fiat_code']: elif key in ['transaction_id', 'device_id', 'crypto_code', 'fiat_code']:
processed_row[key] = str(value) processed_row[key] = str(value)
elif key in ['fiat_amount', 'crypto_amount']: elif key in ['fiat_amount', 'crypto_amount']:
processed_row[key] = int(float(value)) if value else 0 try:
processed_row[key] = int(float(value))
except (ValueError, TypeError):
processed_row[key] = 0 # Fallback to 0 for invalid values
elif key in ['commission_percentage', 'discount']: elif key in ['commission_percentage', 'discount']:
processed_row[key] = float(value) if value else 0.0 try:
processed_row[key] = float(value)
except (ValueError, TypeError):
processed_row[key] = 0.0 # Fallback to 0.0 for invalid values
elif key == 'transaction_time': elif key == 'transaction_time':
from datetime import datetime from datetime import datetime
# Parse PostgreSQL timestamp format and ensure it's in UTC for consistency # Parse PostgreSQL timestamp format and ensure it's in UTC for consistency
@ -548,6 +560,44 @@ class LamassuTransactionProcessor:
logger.error(f"Error executing SSH query: {e}") logger.error(f"Error executing SSH query: {e}")
return [] 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]]: 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""" """Fetch new successful transactions from Lamassu database since last poll"""
try: try:
@ -561,13 +611,13 @@ class LamassuTransactionProcessor:
# Fallback to last 24 hours for first run or if no previous poll # Fallback to last 24 hours for first run or if no previous poll
time_threshold = datetime.now(timezone.utc) - timedelta(hours=24) time_threshold = datetime.now(timezone.utc) - timedelta(hours=24)
logger.info(f"No previous poll found, checking last 24 hours since: {time_threshold}") logger.info(f"No previous poll found, checking last 24 hours since: {time_threshold}")
# Convert to UTC if not already timezone-aware # Convert to UTC if not already timezone-aware
if time_threshold.tzinfo is None: if time_threshold.tzinfo is None:
time_threshold = time_threshold.replace(tzinfo=timezone.utc) time_threshold = time_threshold.replace(tzinfo=timezone.utc)
elif time_threshold.tzinfo != timezone.utc: elif time_threshold.tzinfo != timezone.utc:
time_threshold = time_threshold.astimezone(timezone.utc) time_threshold = time_threshold.astimezone(timezone.utc)
# Format as UTC for database query # Format as UTC for database query
time_threshold_str = time_threshold.strftime('%Y-%m-%d %H:%M:%S UTC') time_threshold_str = time_threshold.strftime('%Y-%m-%d %H:%M:%S UTC')
@ -628,11 +678,11 @@ class LamassuTransactionProcessor:
logger.info("No Flow Mode clients found - skipping distribution") logger.info("No Flow Mode clients found - skipping distribution")
return {} return {}
# Extract transaction details with None-safe defaults # Extract transaction details - guaranteed clean from data ingestion
crypto_atoms = transaction.get("crypto_amount") # Total sats with commission baked in crypto_atoms = transaction.get("crypto_amount", 0) # Total sats with commission baked in
fiat_amount = transaction.get("fiat_amount") # Actual fiat dispensed (principal only) fiat_amount = transaction.get("fiat_amount", 0) # Actual fiat dispensed (principal only)
commission_percentage = transaction.get("commission_percentage") # Already stored as decimal (e.g., 0.045) commission_percentage = transaction.get("commission_percentage", 0.0) # Already stored as decimal (e.g., 0.045)
discount = transaction.get("discount") # Discount percentage discount = transaction.get("discount", 0.0) # Discount percentage
transaction_time = transaction.get("transaction_time") # ATM transaction timestamp for temporal accuracy transaction_time = transaction.get("transaction_time") # ATM transaction timestamp for temporal accuracy
# Normalize transaction_time to UTC if present # Normalize transaction_time to UTC if present
@ -837,10 +887,13 @@ class LamassuTransactionProcessor:
logger.error(f"CRITICAL: Client {client_id[:8]}... has negative balance ({current_balance.remaining_balance:.2f} GTQ) - REFUSING payment of {distribution['sats_amount']} sats") logger.error(f"CRITICAL: Client {client_id[:8]}... has negative balance ({current_balance.remaining_balance:.2f} GTQ) - REFUSING payment of {distribution['sats_amount']} sats")
continue continue
# Verify balance is sufficient for this distribution # Verify balance is sufficient for this distribution (round to 2 decimal places to match DECIMAL(10,2) precision)
fiat_equivalent = distribution["fiat_amount"] # Amount in GTQ fiat_equivalent = distribution["fiat_amount"] # Amount in GTQ
if current_balance.remaining_balance < fiat_equivalent: # Round both values to 2 decimal places to match database precision and avoid floating point comparison issues
logger.error(f"CRITICAL: Client {client_id[:8]}... insufficient balance ({current_balance.remaining_balance:.2f} < {fiat_equivalent:.2f} GTQ) - REFUSING payment") balance_rounded = round(current_balance.remaining_balance, 2)
amount_rounded = round(fiat_equivalent, 2)
if balance_rounded < amount_rounded:
logger.error(f"CRITICAL: Client {client_id[:8]}... insufficient balance ({balance_rounded:.2f} < {amount_rounded:.2f} GTQ) - REFUSING payment")
continue continue
logger.info(f"Client {client_id[:8]}... pre-payment balance check: {current_balance.remaining_balance:.2f} GTQ - SUFFICIENT for {fiat_equivalent:.2f} GTQ payment") logger.info(f"Client {client_id[:8]}... pre-payment balance check: {current_balance.remaining_balance:.2f} GTQ - SUFFICIENT for {fiat_equivalent:.2f} GTQ payment")
@ -995,11 +1048,11 @@ class LamassuTransactionProcessor:
async def store_lamassu_transaction(self, transaction: Dict[str, Any]) -> Optional[str]: async def store_lamassu_transaction(self, transaction: Dict[str, Any]) -> Optional[str]:
"""Store the Lamassu transaction in our database for audit and UI""" """Store the Lamassu transaction in our database for audit and UI"""
try: try:
# Extract and validate transaction data # Extract transaction data - guaranteed clean from data ingestion boundary
crypto_atoms = transaction.get("crypto_amount", 0) crypto_atoms = transaction.get("crypto_amount", 0)
fiat_amount = transaction.get("fiat_amount", 0) fiat_amount = transaction.get("fiat_amount", 0)
commission_percentage = transaction.get("commission_percentage") or 0.0 commission_percentage = transaction.get("commission_percentage", 0.0)
discount = transaction.get("discount") or 0.0 discount = transaction.get("discount", 0.0)
transaction_time = transaction.get("transaction_time") transaction_time = transaction.get("transaction_time")
# Normalize transaction_time to UTC if present # Normalize transaction_time to UTC if present
@ -1145,8 +1198,8 @@ class LamassuTransactionProcessor:
# Calculate commission amount for sending to commission wallet # Calculate commission amount for sending to commission wallet
crypto_atoms = transaction.get("crypto_amount", 0) crypto_atoms = transaction.get("crypto_amount", 0)
commission_percentage = transaction.get("commission_percentage") or 0.0 commission_percentage = transaction.get("commission_percentage", 0.0)
discount = transaction.get("discount") or 0.0 discount = transaction.get("discount", 0.0)
if commission_percentage and commission_percentage > 0: if commission_percentage and commission_percentage > 0:
effective_commission = commission_percentage * (100 - discount) / 100 effective_commission = commission_percentage * (100 - discount) / 100

View file

@ -233,69 +233,139 @@ async def api_manual_poll(
) )
@satmachineadmin_api_router.post("/api/v1/dca/test-transaction") @satmachineadmin_api_router.post("/api/v1/dca/process-transaction/{transaction_id}")
async def api_test_transaction( async def api_process_specific_transaction(
transaction_id: str,
user: User = Depends(check_super_user), user: User = Depends(check_super_user),
crypto_atoms: int = 103, ):
commission_percentage: float = 0.03, """
discount: float = 0.0, Manually process a specific Lamassu transaction by ID, bypassing all status filters.
) -> dict:
"""Test transaction processing with simulated Lamassu transaction data""" This endpoint is useful for processing transactions that were manually settled
or had dispense issues but need to be included in DCA distribution.
"""
try: try:
from .transaction_processor import transaction_processor from .transaction_processor import transaction_processor
import uuid from .crud import get_payments_by_lamassu_transaction
from datetime import datetime, timezone
# Create a mock transaction that mimics Lamassu database structure # Get database configuration
mock_transaction = { db_config = await transaction_processor.connect_to_lamassu_db()
"transaction_id": str(uuid.uuid4())[:8], # Short ID for testing if not db_config:
"crypto_amount": crypto_atoms, # Total sats including commission raise HTTPException(
"fiat_amount": 100, # Mock fiat amount (100 centavos = 1 GTQ) status_code=HTTPStatus.SERVICE_UNAVAILABLE,
"commission_percentage": commission_percentage, # Already as decimal detail="Could not get Lamassu database configuration",
"discount": discount, )
"transaction_time": datetime.now(timezone.utc),
"crypto_code": "BTC",
"fiat_code": "GTQ",
"device_id": "test_device",
"status": "confirmed",
}
# Process the mock transaction through the complete DCA flow # Check if transaction was already processed
await transaction_processor.process_transaction(mock_transaction) 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),
}
# Calculate commission for response # Fetch the specific transaction from Lamassu (bypassing all filters)
if commission_percentage > 0: transaction = await transaction_processor.fetch_transaction_by_id(db_config, transaction_id)
effective_commission = commission_percentage * (100 - discount) / 100
base_crypto_atoms = int(crypto_atoms / (1 + effective_commission)) if not transaction:
commission_amount_sats = crypto_atoms - base_crypto_atoms raise HTTPException(
else: status_code=HTTPStatus.NOT_FOUND,
base_crypto_atoms = crypto_atoms detail=f"Transaction {transaction_id} not found in Lamassu database",
commission_amount_sats = 0 )
# Process the transaction through normal DCA flow
await transaction_processor.process_transaction(transaction)
return { return {
"success": True, "success": True,
"message": "Test transaction processed successfully", "message": f"Transaction {transaction_id} processed successfully",
"transaction_details": { "transaction_details": {
"transaction_id": mock_transaction["transaction_id"], "transaction_id": transaction_id,
"total_amount_sats": crypto_atoms, "status": transaction.get("status"),
"base_amount_sats": base_crypto_atoms, "dispense": transaction.get("dispense"),
"commission_amount_sats": commission_amount_sats, "dispense_confirmed": transaction.get("dispense_confirmed"),
"commission_percentage": commission_percentage "crypto_amount": transaction.get("crypto_amount"),
* 100, # Show as percentage "fiat_amount": transaction.get("fiat_amount"),
"effective_commission": effective_commission * 100
if commission_percentage > 0
else 0,
"discount": discount,
}, },
} }
except HTTPException:
raise
except Exception as e: except Exception as e:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=f"Error processing test transaction: {str(e)}", detail=f"Error processing transaction {transaction_id}: {str(e)}",
) )
# COMMENTED OUT FOR PRODUCTION - Test transaction endpoint disabled
# Uncomment only for development/debugging purposes
#
# @satmachineadmin_api_router.post("/api/v1/dca/test-transaction")
# async def api_test_transaction(
# user: User = Depends(check_super_user),
# crypto_atoms: int = 103,
# commission_percentage: float = 0.03,
# discount: float = 0.0,
# ) -> dict:
# """Test transaction processing with simulated Lamassu transaction data"""
# try:
# from .transaction_processor import transaction_processor
# import uuid
# from datetime import datetime, timezone
#
# # Create a mock transaction that mimics Lamassu database structure
# mock_transaction = {
# "transaction_id": str(uuid.uuid4())[:8], # Short ID for testing
# "crypto_amount": crypto_atoms, # Total sats including commission
# "fiat_amount": 100, # Mock fiat amount (100 centavos = 1 GTQ)
# "commission_percentage": commission_percentage, # Already as decimal
# "discount": discount,
# "transaction_time": datetime.now(timezone.utc),
# "crypto_code": "BTC",
# "fiat_code": "GTQ",
# "device_id": "test_device",
# "status": "confirmed",
# }
#
# # Process the mock transaction through the complete DCA flow
# await transaction_processor.process_transaction(mock_transaction)
#
# # Calculate commission for response
# if commission_percentage > 0:
# effective_commission = commission_percentage * (100 - discount) / 100
# base_crypto_atoms = int(crypto_atoms / (1 + effective_commission))
# commission_amount_sats = crypto_atoms - base_crypto_atoms
# else:
# base_crypto_atoms = crypto_atoms
# commission_amount_sats = 0
#
# return {
# "success": True,
# "message": "Test transaction processed successfully",
# "transaction_details": {
# "transaction_id": mock_transaction["transaction_id"],
# "total_amount_sats": crypto_atoms,
# "base_amount_sats": base_crypto_atoms,
# "commission_amount_sats": commission_amount_sats,
# "commission_percentage": commission_percentage
# * 100, # Show as percentage
# "effective_commission": effective_commission * 100
# if commission_percentage > 0
# else 0,
# "discount": discount,
# },
# }
#
# except Exception as e:
# raise HTTPException(
# status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
# detail=f"Error processing test transaction: {str(e)}",
# )
# Lamassu Transaction Endpoints # Lamassu Transaction Endpoints