Clear out

This commit is contained in:
padreug 2025-06-22 11:53:23 +02:00
parent 462c93d18d
commit 8328861e22
4 changed files with 14 additions and 1769 deletions

40
crud.py
View file

@ -6,42 +6,11 @@ from datetime import datetime, timezone
from lnbits.db import Database
from lnbits.helpers import urlsafe_short_hash
from .models import (
CreateDcaClientData, DcaClient, UpdateDcaClientData,
CreateDepositData, DcaDeposit, UpdateDepositStatusData,
CreateDcaPaymentData, DcaPayment,
ClientBalanceSummary,
CreateLamassuConfigData, LamassuConfig, UpdateLamassuConfigData,
CreateLamassuTransactionData, StoredLamassuTransaction
)
from .models import ()
db = Database("ext_satmachineclient")
# DCA Client CRUD Operations
async def create_dca_client(data: CreateDcaClientData) -> DcaClient:
client_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO satmachineclient.dca_clients
(id, user_id, wallet_id, username, dca_mode, fixed_mode_daily_limit, status, created_at, updated_at)
VALUES (:id, :user_id, :wallet_id, :username, :dca_mode, :fixed_mode_daily_limit, :status, :created_at, :updated_at)
""",
{
"id": client_id,
"user_id": data.user_id,
"wallet_id": data.wallet_id,
"username": data.username,
"dca_mode": data.dca_mode,
"fixed_mode_daily_limit": data.fixed_mode_daily_limit,
"status": "active",
"created_at": datetime.now(),
"updated_at": datetime.now()
}
)
return await get_dca_client(client_id)
async def get_dca_client(client_id: str) -> Optional[DcaClient]:
return await db.fetchone(
"SELECT * FROM satmachineclient.dca_clients WHERE id = :id",
@ -50,13 +19,6 @@ async def get_dca_client(client_id: str) -> Optional[DcaClient]:
)
async def get_dca_clients() -> List[DcaClient]:
return await db.fetchall(
"SELECT * FROM satmachineclient.dca_clients ORDER BY created_at DESC",
model=DcaClient,
)
async def get_dca_client_by_user(user_id: str) -> Optional[DcaClient]:
return await db.fetchone(
"SELECT * FROM satmachineclient.dca_clients WHERE user_id = :user_id",

View file

@ -4,122 +4,6 @@ window.app = Vue.createApp({
delimiters: ['${', '}'],
data: function () {
return {
// DCA Client Data
dcaClients: [],
deposits: [],
lamassuTransactions: [],
// Table configurations
clientsTable: {
columns: [
{ name: 'username', align: 'left', label: 'Username', field: 'username' },
{ name: 'user_id', align: 'left', label: 'User ID', field: 'user_id' },
{ name: 'wallet_id', align: 'left', label: 'Wallet ID', field: 'wallet_id' },
{ name: 'dca_mode', align: 'left', label: 'DCA Mode', field: 'dca_mode' },
{ name: 'remaining_balance', align: 'right', label: 'Remaining Balance', field: 'remaining_balance' },
{ name: 'fixed_mode_daily_limit', align: 'left', label: 'Daily Limit', field: 'fixed_mode_daily_limit' },
{ name: 'status', align: 'left', label: 'Status', field: 'status' }
],
pagination: {
rowsPerPage: 10
}
},
depositsTable: {
columns: [
{ name: 'client_id', align: 'left', label: 'Client', field: 'client_id' },
{ name: 'amount', align: 'left', label: 'Amount', field: 'amount' },
{ name: 'currency', align: 'left', label: 'Currency', field: 'currency' },
{ name: 'status', align: 'left', label: 'Status', field: 'status' },
{ name: 'created_at', align: 'left', label: 'Created', field: 'created_at' },
{ name: 'notes', align: 'left', label: 'Notes', field: 'notes' }
],
pagination: {
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: {
show: false,
data: {
currency: 'GTQ'
}
},
clientDetailsDialog: {
show: false,
data: null,
balance: null
},
distributionDialog: {
show: false,
transaction: null,
distributions: []
},
// Quick deposit form
quickDepositForm: {
selectedClient: null,
amount: null,
notes: ''
},
// Polling status
lastPollTime: null,
testingConnection: false,
runningManualPoll: false,
runningTestTransaction: false,
lamassuConfig: null,
// Config dialog
configDialog: {
show: false,
data: {
host: '',
port: 5432,
database_name: '',
username: '',
password: '',
selectedWallet: null,
selectedCommissionWallet: null,
// SSH Tunnel settings
use_ssh_tunnel: false,
ssh_host: '',
ssh_port: 22,
ssh_username: '',
ssh_password: '',
ssh_private_key: ''
}
},
// Options
currencyOptions: [
{ label: 'GTQ', value: 'GTQ' },
{ label: 'USD', value: 'USD' }
]
}
},
@ -154,535 +38,16 @@ window.app = Vue.createApp({
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
},
// Configuration Methods
async getLamassuConfig() {
try {
const {data} = await LNbits.api.request(
'GET',
'/satmachineclient/api/v1/dca/config',
this.g.user.wallets[0].inkey
)
this.lamassuConfig = data
// When opening config dialog, populate the selected wallets if they exist
if (data && data.source_wallet_id) {
const wallet = this.g.user.wallets.find(w => w.id === data.source_wallet_id)
if (wallet) {
this.configDialog.data.selectedWallet = wallet
}
}
if (data && data.commission_wallet_id) {
const commissionWallet = this.g.user.wallets.find(w => w.id === data.commission_wallet_id)
if (commissionWallet) {
this.configDialog.data.selectedCommissionWallet = commissionWallet
}
}
} catch (error) {
// It's OK if no config exists yet
this.lamassuConfig = null
}
},
async saveConfiguration() {
try {
const data = {
host: this.configDialog.data.host,
port: this.configDialog.data.port,
database_name: this.configDialog.data.database_name,
username: this.configDialog.data.username,
password: this.configDialog.data.password,
source_wallet_id: this.configDialog.data.selectedWallet?.id,
commission_wallet_id: this.configDialog.data.selectedCommissionWallet?.id,
// SSH Tunnel settings
use_ssh_tunnel: this.configDialog.data.use_ssh_tunnel,
ssh_host: this.configDialog.data.ssh_host,
ssh_port: this.configDialog.data.ssh_port,
ssh_username: this.configDialog.data.ssh_username,
ssh_password: this.configDialog.data.ssh_password,
ssh_private_key: this.configDialog.data.ssh_private_key
}
const {data: config} = await LNbits.api.request(
'POST',
'/satmachineclient/api/v1/dca/config',
this.g.user.wallets[0].adminkey,
data
)
this.lamassuConfig = config
this.closeConfigDialog()
this.$q.notify({
type: 'positive',
message: 'Database configuration saved successfully',
timeout: 5000
})
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
closeConfigDialog() {
this.configDialog.show = false
this.configDialog.data = {
host: '',
port: 5432,
database_name: '',
username: '',
password: '',
selectedWallet: null,
selectedCommissionWallet: null,
// SSH Tunnel settings
use_ssh_tunnel: false,
ssh_host: '',
ssh_port: 22,
ssh_username: '',
ssh_password: '',
ssh_private_key: ''
}
},
// DCA Client Methods
async getDcaClients() {
try {
const { data } = await LNbits.api.request(
'GET',
'/satmachineclient/api/v1/dca/clients',
this.g.user.wallets[0].inkey
)
// Fetch balance data for each client
const clientsWithBalances = await Promise.all(
data.map(async (client) => {
try {
const { data: balance } = await LNbits.api.request(
'GET',
`/satmachineclient/api/v1/dca/clients/${client.id}/balance`,
this.g.user.wallets[0].inkey
)
return {
...client,
remaining_balance: balance.remaining_balance
}
} catch (error) {
console.error(`Error fetching balance for client ${client.id}:`, error)
return {
...client,
remaining_balance: 0
}
}
})
)
this.dcaClients = clientsWithBalances
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
// Test Client Creation (temporary for testing)
async createTestClient() {
try {
const testData = {
user_id: this.g.user.id,
wallet_id: this.g.user.wallets[0].id,
username: this.g.user.username || `user_${this.g.user.id.substring(0, 8)}`,
dca_mode: 'flow'
}
const { data: newClient } = await LNbits.api.request(
'POST',
'/satmachineclient/api/v1/dca/clients',
this.g.user.wallets[0].adminkey,
testData
)
this.dcaClients.push(newClient)
this.$q.notify({
type: 'positive',
message: 'Test client created successfully!',
timeout: 5000
})
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
// Quick Deposit Methods
async sendQuickDeposit() {
try {
const data = {
client_id: this.quickDepositForm.selectedClient?.value,
amount: this.quickDepositForm.amount,
currency: 'GTQ',
notes: this.quickDepositForm.notes
}
const { data: newDeposit } = await LNbits.api.request(
'POST',
'/satmachineclient/api/v1/dca/deposits',
this.g.user.wallets[0].adminkey,
data
)
this.deposits.unshift(newDeposit)
// Reset form
this.quickDepositForm = {
selectedClient: null,
amount: null,
notes: ''
}
this.$q.notify({
type: 'positive',
message: 'Deposit created successfully',
timeout: 5000
})
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
async viewClientDetails(client) {
try {
const { data: balance } = await LNbits.api.request(
'GET',
`/satmachineclient/api/v1/dca/clients/${client.id}/balance`,
this.g.user.wallets[0].inkey
)
this.clientDetailsDialog.data = client
this.clientDetailsDialog.balance = balance
this.clientDetailsDialog.show = true
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
// Deposit Methods
async getDeposits() {
try {
const { data } = await LNbits.api.request(
'GET',
'/satmachineclient/api/v1/dca/deposits',
this.g.user.wallets[0].inkey
)
this.deposits = data
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
addDepositDialog(client) {
this.depositFormDialog.data = {
client_id: client.id,
client_name: client.username || `${client.user_id.substring(0, 8)}...`,
currency: 'GTQ'
}
this.depositFormDialog.show = true
},
async sendDepositData() {
try {
const data = {
client_id: this.depositFormDialog.data.client_id,
amount: this.depositFormDialog.data.amount,
currency: this.depositFormDialog.data.currency,
notes: this.depositFormDialog.data.notes
}
if (this.depositFormDialog.data.id) {
// Update existing deposit (mainly for notes/status)
const { data: updatedDeposit } = await LNbits.api.request(
'PUT',
`/satmachineclient/api/v1/dca/deposits/${this.depositFormDialog.data.id}`,
this.g.user.wallets[0].adminkey,
{ status: this.depositFormDialog.data.status, notes: data.notes }
)
const index = this.deposits.findIndex(d => d.id === updatedDeposit.id)
if (index !== -1) {
this.deposits.splice(index, 1, updatedDeposit)
}
} else {
// Create new deposit
const { data: newDeposit } = await LNbits.api.request(
'POST',
'/satmachineclient/api/v1/dca/deposits',
this.g.user.wallets[0].adminkey,
data
)
this.deposits.unshift(newDeposit)
}
this.closeDepositFormDialog()
this.$q.notify({
type: 'positive',
message: this.depositFormDialog.data.id ? 'Deposit updated successfully' : 'Deposit created successfully',
timeout: 5000
})
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
closeDepositFormDialog() {
this.depositFormDialog.show = false
this.depositFormDialog.data = {
currency: 'GTQ'
}
},
async confirmDeposit(deposit) {
try {
await LNbits.utils
.confirmDialog('Confirm that this deposit has been physically placed in the ATM machine?')
.onOk(async () => {
const { data: updatedDeposit } = await LNbits.api.request(
'PUT',
`/satmachineclient/api/v1/dca/deposits/${deposit.id}/status`,
this.g.user.wallets[0].adminkey,
{ status: 'confirmed', notes: 'Confirmed by admin - money placed in machine' }
)
const index = this.deposits.findIndex(d => d.id === deposit.id)
if (index !== -1) {
this.deposits.splice(index, 1, updatedDeposit)
}
this.$q.notify({
type: 'positive',
message: 'Deposit confirmed! DCA is now active for this client.',
timeout: 5000
})
})
} catch (error) {
LNbits.utils.notifyApiError(error)
}
},
editDeposit(deposit) {
this.depositFormDialog.data = { ...deposit }
this.depositFormDialog.show = true
},
// Export Methods
async exportClientsCSV() {
await LNbits.utils.exportCSV(this.clientsTable.columns, this.dcaClients)
},
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() {
this.testingConnection = true
try {
const {data} = await LNbits.api.request(
'POST',
'/satmachineclient/api/v1/dca/test-connection',
this.g.user.wallets[0].adminkey
)
// Show detailed results in a dialog
const stepsList = data.steps ? data.steps.join('\n') : 'No detailed steps available'
let dialogContent = `<strong>Connection Test Results</strong><br/><br/>`
if (data.ssh_tunnel_used) {
dialogContent += `<strong>SSH Tunnel:</strong> ${data.ssh_tunnel_success ? '✅ Success' : '❌ Failed'}<br/>`
}
dialogContent += `<strong>Database:</strong> ${data.database_connection_success ? '✅ Success' : '❌ Failed'}<br/><br/>`
dialogContent += `<strong>Detailed Steps:</strong><br/>`
dialogContent += stepsList.replace(/\n/g, '<br/>')
this.$q.dialog({
title: data.success ? 'Connection Test Passed' : 'Connection Test Failed',
message: dialogContent,
html: true,
ok: {
color: data.success ? 'positive' : 'negative',
label: 'Close'
}
})
// Also show a brief notification
this.$q.notify({
type: data.success ? 'positive' : 'negative',
message: data.message,
timeout: 3000
})
} catch (error) {
LNbits.utils.notifyApiError(error)
} finally {
this.testingConnection = false
}
},
async manualPoll() {
this.runningManualPoll = true
try {
const {data} = await LNbits.api.request(
'POST',
'/satmachineclient/api/v1/dca/manual-poll',
this.g.user.wallets[0].adminkey
)
this.lastPollTime = new Date().toLocaleString()
this.$q.notify({
type: 'positive',
message: `Manual poll completed. Found ${data.transactions_processed} new transactions.`,
timeout: 5000
})
// 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)
} finally {
this.runningManualPoll = false
}
},
async testTransaction() {
this.runningTestTransaction = true
try {
const {data} = await LNbits.api.request(
'POST',
'/satmachineclient/api/v1/dca/test-transaction',
this.g.user.wallets[0].adminkey
)
// Show detailed results in a dialog
const details = data.transaction_details
let dialogContent = `<strong>Test Transaction Results</strong><br/><br/>`
dialogContent += `<strong>Transaction ID:</strong> ${details.transaction_id}<br/>`
dialogContent += `<strong>Total Amount:</strong> ${details.total_amount_sats} sats<br/>`
dialogContent += `<strong>Base Amount:</strong> ${details.base_amount_sats} sats<br/>`
dialogContent += `<strong>Commission:</strong> ${details.commission_amount_sats} sats (${details.commission_percentage}%)<br/>`
if (details.discount > 0) {
dialogContent += `<strong>Discount:</strong> ${details.discount}%<br/>`
dialogContent += `<strong>Effective Commission:</strong> ${details.effective_commission}%<br/>`
}
dialogContent += `<br/><strong>Check your wallets to see the distributions!</strong>`
this.$q.dialog({
title: 'Test Transaction Completed',
message: dialogContent,
html: true,
ok: {
color: 'positive',
label: 'Great!'
}
})
// Also show a brief notification
this.$q.notify({
type: 'positive',
message: `Test transaction processed: ${details.total_amount_sats} sats distributed`,
timeout: 5000
})
// 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)
} finally {
this.runningTestTransaction = false
}
},
// Lamassu Transaction Methods
async getLamassuTransactions() {
try {
const { data } = await LNbits.api.request(
'GET',
'/satmachineclient/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',
`/satmachineclient/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)
}
},
},
///////////////////////////////////////////////////
//////LIFECYCLE FUNCTIONS RUNNING ON PAGE LOAD/////
///////////////////////////////////////////////////
async created() {
// Load DCA admin data
// Load DCA client data
await Promise.all([
this.getLamassuConfig(),
this.getDcaClients(),
this.getDeposits(),
this.getLamassuTransactions()
])
},
computed: {
isConfigFormValid() {
const data = this.configDialog.data
// Basic database fields are required
const basicValid = data.host && data.database_name && data.username && data.selectedWallet
// If SSH tunnel is enabled, validate SSH fields
if (data.use_ssh_tunnel) {
const sshValid = data.ssh_host && data.ssh_username &&
(data.ssh_password || data.ssh_private_key)
return basicValid && sshValid
}
return basicValid
},
clientOptions() {
return this.dcaClients.map(client => ({
label: `${client.username || client.user_id.substring(0, 8) + '...'} (${client.dca_mode})`,
value: client.id
}))
},
totalDcaBalance() {
return this.deposits
.filter(d => d.status === 'confirmed')
.reduce((total, deposit) => total + deposit.amount, 0)
}
}
})

View file

@ -7,765 +7,6 @@
<script src="{{ static_url_for('satmachineclient/static', path='js/index.js') }}"></script>
{% endblock %} {% block page %}
<div class="row q-col-gutter-md" id="dcaClient">
<div class="col-12 col-md-8 col-lg-7 q-gutter-y-md">
<!-- Deposit Management Section -->
<q-card>
<q-card-section>
<div class="row items-center no-wrap q-mb-md">
<div class="col">
<h5 class="text-subtitle1 q-my-none">DCA Deposit Management</h5>
<p class="text-caption q-my-none">Manage fiat deposits for existing DCA clients</p>
</div>
</div>
</q-card-section>
</q-card>
<!-- DCA Clients Table -->
<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">Registered DCA Clients</h6>
<p class="text-caption q-my-none">Clients registered via the DCA client extension</p>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportClientsCSV">Export to CSV</q-btn>
</div>
</div>
<q-table
dense
flat
:rows="dcaClients"
row-key="id"
:columns="clientsTable.columns"
v-model:pagination="clientsTable.pagination"
>
<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 == 'username'">${ col.value || 'No username' }</div>
<div v-else-if="col.field == 'user_id'">${ col.value.substring(0, 8) }...</div>
<div v-else-if="col.field == 'wallet_id'">${ col.value.substring(0, 8) }...</div>
<div v-else-if="col.field == 'status'">
<q-badge :color="col.value === 'active' ? 'green' : 'red'">
${ col.value }
</q-badge>
</div>
<div v-else-if="col.field == 'remaining_balance'">
<span :class="col.value > 0 ? 'text-green-8 text-weight-bold' : 'text-grey-6'">
${ formatCurrency(col.value || 0) }
</span>
</div>
<div v-else-if="col.field == 'fixed_mode_daily_limit' && col.value">
${ formatCurrency(col.value) }
</div>
<div v-else>${ col.value || '-' }</div>
</q-td>
<q-td auto-width>
<q-btn
flat dense size="sm" icon="account_balance_wallet"
color="primary" class="q-mr-sm"
@click="addDepositDialog(props.row)"
>
<q-tooltip>Add Deposit</q-tooltip>
</q-btn>
<q-btn
flat dense size="sm" icon="visibility"
color="blue"
@click="viewClientDetails(props.row)"
>
<q-tooltip>View Balance & Details</q-tooltip>
</q-btn>
</q-td>
</q-tr>
</template>
</q-table>
</q-card-section>
</q-card>
<!-- Quick Add Deposit Section -->
<q-card>
<q-card-section>
<h6 class="text-subtitle2 q-my-none">Quick Add Deposit</h6>
<p class="text-caption q-my-none">Add a new deposit for an existing client</p>
<div v-if="dcaClients.length === 0" class="q-mt-md">
<q-banner class="bg-orange-1 text-orange-9">
<template v-slot:avatar>
<q-icon name="info" color="orange" />
</template>
No DCA clients registered yet. Clients must first install and configure the DCA client extension.
<template v-slot:action>
<q-btn
flat
color="orange"
label="Create Test Client"
@click="createTestClient"
size="sm"
/>
</template>
</q-banner>
</div>
<q-form v-else @submit="sendQuickDeposit" class="q-gutter-md q-mt-md">
<div class="row q-gutter-md">
<div class="col">
<q-select
filled
dense
v-model="quickDepositForm.selectedClient"
:options="clientOptions"
label="Select Client *"
option-label="label"
option-value="value"
></q-select>
</div>
<div class="col">
<q-input
filled
dense
type="number"
v-model.number="quickDepositForm.amount"
label="Amount (GTQ) *"
placeholder="Amount in centavos (GTQ * 100)"
hint="Enter amount in centavos"
></q-input>
</div>
<div class="col-auto">
<q-btn
unelevated
color="primary"
type="submit"
:disable="!quickDepositForm.selectedClient || !quickDepositForm.amount"
>Add Deposit</q-btn
>
</div>
</div>
<div class="row">
<div class="col">
<q-input
filled
dense
type="textarea"
v-model.trim="quickDepositForm.notes"
label="Notes (Optional)"
placeholder="Optional notes about this deposit"
rows="2"
></q-input>
</div>
</div>
</q-form>
</q-card-section>
</q-card>
<!-- Deposits Management 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">Recent Deposits</h6>
</div>
<div class="col-auto">
<q-btn flat color="grey" @click="exportDepositsCSV">Export to CSV</q-btn>
</div>
</div>
<q-table
dense
flat
:rows="deposits"
row-key="id"
:columns="depositsTable.columns"
v-model:pagination="depositsTable.pagination"
>
<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_id'">${ getClientUsername(col.value) }</div>
<div v-else-if="col.field == 'amount'">${ formatCurrency(col.value) }</div>
<div v-else-if="col.field == 'status'">
<q-badge :color="col.value === 'confirmed' ? 'green' : 'orange'">
${ col.value }
</q-badge>
</div>
<div v-else-if="col.field == 'created_at'">${ formatDate(col.value) }</div>
<div v-else>${ col.value }</div>
</q-td>
<q-td auto-width>
<q-btn
v-if="props.row.status === 'pending'"
flat dense size="sm" icon="check_circle"
color="green" class="q-mr-sm"
@click="confirmDeposit(props.row)"
>
<q-tooltip>Confirm Deposit</q-tooltip>
</q-btn>
<q-btn
flat dense size="sm" icon="edit"
color="orange"
@click="editDeposit(props.row)"
>
<q-tooltip>Edit Deposit</q-tooltip>
</q-btn>
</q-td>
</q-tr>
</template>
</q-table>
</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>
</div>
<div class="col-12 col-md-4 col-lg-5 q-gutter-y-md">
<q-card>
<q-card-section>
<h6 class="text-subtitle1 q-my-none">
{{SITE_TITLE}} DCA Client Extension
</h6>
<p>
Dollar Cost Averaging administration for Lamassu ATM integration. <br />
Manage client deposits and DCA distribution settings.
</p>
</q-card-section>
<q-card-section class="q-pa-none">
<q-separator></q-separator>
<q-list>
<q-expansion-item
group="api"
icon="info"
label="DCA System Status"
:content-inset-level="0.5"
>
<q-card-section class="text-caption">
<div class="row">
<div class="col-6">Active Clients:</div>
<div class="col-6">${ dcaClients.filter(c => c.status === 'active').length }</div>
</div>
<div class="row">
<div class="col-6">Pending Deposits:</div>
<div class="col-6">${ deposits.filter(d => d.status === 'pending').length }</div>
</div>
<div class="row">
<div class="col-6">Total DCA Balance:</div>
<div class="col-6">${ formatCurrency(totalDcaBalance) }</div>
</div>
</q-card-section>
</q-expansion-item>
<q-separator></q-separator>
<q-expansion-item
group="api"
icon="settings"
label="Lamassu Database Config"
:content-inset-level="0.5"
>
<q-card-section class="text-caption">
<div v-if="lamassuConfig">
<p><strong>Database:</strong> ${ lamassuConfig.host }:${ lamassuConfig.port }/${ lamassuConfig.database_name }</p>
<p><strong>Status:</strong>
<q-badge v-if="lamassuConfig.test_connection_success === true" color="green">Connected</q-badge>
<q-badge v-else-if="lamassuConfig.test_connection_success === false" color="red">Failed</q-badge>
<q-badge v-else color="grey">Not tested</q-badge>
</p>
<p><strong>Last Poll:</strong> ${ lamassuConfig.last_poll_time ? formatDateTime(lamassuConfig.last_poll_time) : 'Not yet run' }</p>
<p><strong>Last Success:</strong> ${ lamassuConfig.last_successful_poll ? formatDateTime(lamassuConfig.last_successful_poll) : 'Never' }</p>
</div>
<div v-else>
<p><strong>Status:</strong> <q-badge color="orange">Not configured</q-badge></p>
</div>
<div class="q-mt-md">
<q-btn
size="sm"
color="primary"
@click="configDialog.show = true"
icon="settings"
>
Configure Database
</q-btn>
<q-btn
v-if="lamassuConfig"
size="sm"
color="accent"
@click="testDatabaseConnection"
:loading="testingConnection"
class="q-ml-sm"
>
Test Connection
</q-btn>
<q-btn
v-if="lamassuConfig"
size="sm"
color="secondary"
@click="manualPoll"
:loading="runningManualPoll"
class="q-ml-sm"
>
Manual Poll
</q-btn>
<q-btn
v-if="lamassuConfig && lamassuConfig.source_wallet_id"
size="sm"
color="warning"
@click="testTransaction"
:loading="runningTestTransaction"
class="q-ml-sm"
>
Test Transaction
</q-btn>
</div>
</q-card-section>
</q-expansion-item>
<q-separator></q-separator>
{% include "satmachineclient/_api_docs.html" %}
</q-list>
</q-card-section>
</q-card>
</div>
<!--/////////////////////////////////////////////////-->
<!--//////////////DEPOSIT FORM DIALOG////////////////-->
<!--/////////////////////////////////////////////////-->
<q-dialog v-model="depositFormDialog.show" position="top" @hide="closeDepositFormDialog">
<q-card class="q-pa-lg q-pt-xl" style="width: 500px">
<q-form @submit="sendDepositData" class="q-gutter-md">
<div v-if="depositFormDialog.data.client_name" class="text-h6 q-mb-md">
Deposit for: ${ depositFormDialog.data.client_name }
</div>
<q-input
filled
dense
type="number"
v-model.number="depositFormDialog.data.amount"
label="Deposit Amount (GTQ) *"
placeholder="Amount in centavos (GTQ * 100)"
hint="Enter amount in centavos (1 GTQ = 100 centavos)"
></q-input>
<q-select
filled
dense
emit-value
v-model="depositFormDialog.data.currency"
:options="currencyOptions"
label="Currency"
></q-select>
<q-input
filled
dense
type="textarea"
v-model.trim="depositFormDialog.data.notes"
label="Notes"
placeholder="Optional notes about this deposit"
rows="3"
></q-input>
<div class="row q-mt-lg">
<q-btn
v-if="depositFormDialog.data.id"
unelevated
color="primary"
type="submit"
>Update Deposit</q-btn
>
<q-btn
v-else
unelevated
color="primary"
:disable="!depositFormDialog.data.amount"
type="submit"
>Create Deposit</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto"
>Cancel</q-btn
>
</div>
</q-form>
</q-card>
</q-dialog>
<!--/////////////////////////////////////////////////-->
<!--//////////////CLIENT DETAILS DIALOG//////////////-->
<!--/////////////////////////////////////////////////-->
<q-dialog v-model="clientDetailsDialog.show" position="top">
<q-card class="q-pa-lg" style="width: 600px; max-width: 90vw">
<div class="text-h6 q-mb-md">Client Details</div>
<div v-if="clientDetailsDialog.data">
<q-list>
<q-item v-if="clientDetailsDialog.data.username">
<q-item-section>
<q-item-label caption>Username</q-item-label>
<q-item-label>${ clientDetailsDialog.data.username }</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>User ID</q-item-label>
<q-item-label>${ clientDetailsDialog.data.user_id }</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>Wallet ID</q-item-label>
<q-item-label>${ clientDetailsDialog.data.wallet_id }</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>DCA Mode</q-item-label>
<q-item-label>${ clientDetailsDialog.data.dca_mode }</q-item-label>
</q-item-section>
</q-item>
<q-item v-if="clientDetailsDialog.data.fixed_mode_daily_limit">
<q-item-section>
<q-item-label caption>Daily Limit</q-item-label>
<q-item-label>${ formatCurrency(clientDetailsDialog.data.fixed_mode_daily_limit) }</q-item-label>
</q-item-section>
</q-item>
<q-item>
<q-item-section>
<q-item-label caption>Balance Summary</q-item-label>
<q-item-label v-if="clientDetailsDialog.balance">
Deposits: ${ formatCurrency(clientDetailsDialog.balance.total_deposits) } |
Payments: ${ formatCurrency(clientDetailsDialog.balance.total_payments) } |
Remaining: ${ formatCurrency(clientDetailsDialog.balance.remaining_balance) }
</q-item-label>
</q-item-section>
</q-item>
</q-list>
</div>
<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>
<!--/////////////////////////////////////////////////-->
<!--//////////////LAMASSU CONFIG DIALOG//////////////-->
<!--/////////////////////////////////////////////////-->
<q-dialog v-model="configDialog.show" position="top" @hide="closeConfigDialog">
<q-card class="q-pa-lg q-pt-xl" style="width: 600px; max-width: 90vw">
<div class="text-h6 q-mb-md">Lamassu Database Configuration</div>
<q-form @submit="saveConfiguration" class="q-gutter-md">
<q-input
filled
dense
v-model.trim="configDialog.data.host"
label="Database Host *"
placeholder="e.g., localhost or 192.168.1.100"
hint="Hostname or IP address of the Lamassu Postgres server"
></q-input>
<q-input
filled
dense
type="number"
v-model.number="configDialog.data.port"
label="Database Port *"
placeholder="5432"
hint="Postgres port (usually 5432)"
></q-input>
<q-input
filled
dense
v-model.trim="configDialog.data.database_name"
label="Database Name *"
placeholder="lamassu"
hint="Name of the Lamassu database"
></q-input>
<q-input
filled
dense
v-model.trim="configDialog.data.username"
label="Username *"
placeholder="postgres"
hint="Database username with read access"
></q-input>
<q-input
filled
dense
type="password"
v-model.trim="configDialog.data.password"
label="Password *"
placeholder="Enter database password"
hint="Database password"
></q-input>
<q-separator class="q-my-md"></q-separator>
<div class="text-h6 q-mb-md">DCA Source Wallet</div>
<q-select
filled
dense
:options="g.user.wallets"
v-model="configDialog.data.selectedWallet"
label="Source Wallet for DCA Distributions *"
option-label="name"
hint="Wallet that holds Bitcoin for distribution to DCA clients"
></q-select>
<q-select
filled
dense
:options="g.user.wallets"
v-model="configDialog.data.selectedCommissionWallet"
label="Commission Wallet (Optional)"
option-label="name"
hint="Wallet where commission earnings will be sent (leave empty to keep in source wallet)"
></q-select>
<q-separator class="q-my-md"></q-separator>
<div class="text-h6 q-mb-md">SSH Tunnel (Recommended)</div>
<div class="row items-center q-mb-md">
<q-toggle
v-model="configDialog.data.use_ssh_tunnel"
color="primary"
@click.stop
/>
<span class="q-ml-sm">Use SSH Tunnel</span>
</div>
<div v-if="configDialog.data.use_ssh_tunnel" class="q-mt-md" @click.stop>
<q-input
filled
dense
v-model.trim="configDialog.data.ssh_host"
label="SSH Host *"
placeholder="e.g., your-server.com or 192.168.1.100"
hint="SSH server hostname or IP address"
@click.stop
></q-input>
<q-input
filled
dense
type="number"
v-model.number="configDialog.data.ssh_port"
label="SSH Port *"
placeholder="22"
hint="SSH port (usually 22)"
@click.stop
></q-input>
<q-input
filled
dense
v-model.trim="configDialog.data.ssh_username"
label="SSH Username *"
placeholder="ubuntu"
hint="SSH username"
@click.stop
></q-input>
<q-input
filled
dense
type="password"
v-model.trim="configDialog.data.ssh_password"
label="SSH Password"
placeholder="SSH password (if not using key)"
hint="SSH password or leave empty to use private key"
@click.stop
></q-input>
<q-input
filled
dense
type="textarea"
v-model.trim="configDialog.data.ssh_private_key"
label="SSH Private Key"
placeholder="-----BEGIN OPENSSH PRIVATE KEY-----"
hint="SSH private key content (alternative to password)"
rows="4"
@click.stop
></q-input>
<q-banner class="bg-green-1 text-green-9 q-mt-md">
<template v-slot:avatar>
<q-icon name="security" color="green" />
</template>
SSH tunneling keeps your database secure by avoiding direct internet exposure.
The database connection will be routed through the SSH server.
</q-banner>
</div>
<q-banner v-if="!configDialog.data.id" class="bg-blue-1 text-blue-9">
<template v-slot:avatar>
<q-icon name="info" color="blue" />
</template>
This configuration will be securely stored and used for hourly polling.
Only read access to the Lamassu database is required.
</q-banner>
<div class="row q-mt-lg">
<q-btn
unelevated
color="primary"
type="submit"
:disable="!isConfigFormValid"
@click.stop
>Save Configuration</q-btn
>
<q-btn v-close-popup flat color="grey" class="q-ml-auto" @click.stop
>Cancel</q-btn
>
</div>
</q-form>
</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>
</div>
{% endblock %}

View file

@ -7,40 +7,23 @@ from fastapi import APIRouter, Depends, Request
from lnbits.core.crud import get_user
from lnbits.core.models import WalletTypeInfo
from lnbits.core.services import create_invoice
from lnbits.decorators import require_admin_key, require_invoice_key
from lnbits.decorators import require_admin_key
from starlette.exceptions import HTTPException
from .crud import (
# DCA CRUD operations
create_dca_client,
get_dca_clients,
get_dca_client,
update_dca_client,
delete_dca_client,
create_deposit,
get_all_deposits,
get_deposit,
update_deposit_status,
get_client_balance_summary,
# Lamassu config CRUD operations
create_lamassu_config,
get_lamassu_config,
get_active_lamassu_config,
get_all_lamassu_configs,
update_lamassu_config,
update_config_test_result,
delete_lamassu_config,
# Lamassu transaction CRUD operations
get_all_lamassu_transactions,
get_lamassu_transaction
)
from .models import (
# DCA models
CreateDcaClientData, DcaClient, UpdateDcaClientData,
CreateDepositData, DcaDeposit, UpdateDepositStatusData,
CreateDcaClientData,
DcaClient,
DcaDeposit,
ClientBalanceSummary,
CreateLamassuConfigData, LamassuConfig, UpdateLamassuConfigData,
StoredLamassuTransaction
)
satmachineclient_api_router = APIRouter()
@ -51,46 +34,21 @@ satmachineclient_api_router = APIRouter()
###################################################
# DCA Client Endpoints
@satmachineclient_api_router.get("/api/v1/dca/clients")
async def api_get_dca_clients(
wallet: WalletTypeInfo = Depends(require_invoice_key),
) -> list[DcaClient]:
"""Get all DCA clients"""
return await get_dca_clients()
# Note: Client creation/update
@satmachineclient_api_router.get("/api/v1/dca/clients/{client_id}")
async def api_get_dca_client(
client_id: str,
wallet: WalletTypeInfo = Depends(require_invoice_key),
) -> DcaClient:
"""Get a specific DCA client"""
client = await get_dca_client(client_id)
if not client:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="DCA client not found."
)
return client
# Note: Client creation/update/delete will be handled by the DCA client extension
# Admin extension only reads existing clients and manages their deposits
# TEMPORARY: Test client creation endpoint (remove in production)
@satmachineclient_api_router.post("/api/v1/dca/clients", status_code=HTTPStatus.CREATED)
async def api_create_test_dca_client(
data: CreateDcaClientData,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> DcaClient:
"""Create a test DCA client (temporary for testing)"""
return await create_dca_client(data)
@satmachineclient_api_router.get("/api/v1/dca/clients/{client_id}/balance")
async def api_get_client_balance(
client_id: str,
wallet: WalletTypeInfo = Depends(require_invoice_key),
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> ClientBalanceSummary:
"""Get client balance summary"""
client = await get_dca_client(client_id)
@ -104,18 +62,21 @@ async def api_get_client_balance(
# DCA Deposit Endpoints
# NOTE: to Claude - modify this so it only gets the deposits for the user! important security
@satmachineclient_api_router.get("/api/v1/dca/deposits")
async def api_get_deposits(
wallet: WalletTypeInfo = Depends(require_invoice_key),
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> list[DcaDeposit]:
"""Get all deposits"""
return await get_all_deposits()
# NOTE: does the client have any need to get sepcific deposits?
@satmachineclient_api_router.get("/api/v1/dca/deposits/{deposit_id}")
async def api_get_deposit(
deposit_id: str,
wallet: WalletTypeInfo = Depends(require_invoice_key),
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> DcaDeposit:
"""Get a specific deposit"""
deposit = await get_deposit(deposit_id)
@ -124,287 +85,3 @@ async def api_get_deposit(
status_code=HTTPStatus.NOT_FOUND, detail="Deposit not found."
)
return deposit
@satmachineclient_api_router.post("/api/v1/dca/deposits", status_code=HTTPStatus.CREATED)
async def api_create_deposit(
data: CreateDepositData,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> DcaDeposit:
"""Create a new deposit"""
# Verify client exists
client = await get_dca_client(data.client_id)
if not client:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="DCA client not found."
)
return await create_deposit(data)
@satmachineclient_api_router.put("/api/v1/dca/deposits/{deposit_id}/status")
async def api_update_deposit_status(
deposit_id: str,
data: UpdateDepositStatusData,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> DcaDeposit:
"""Update deposit status (e.g., confirm deposit)"""
deposit = await get_deposit(deposit_id)
if not deposit:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Deposit not found."
)
updated_deposit = await update_deposit_status(deposit_id, data)
if not updated_deposit:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Failed to update deposit."
)
return updated_deposit
# Transaction Polling Endpoints
@satmachineclient_api_router.post("/api/v1/dca/test-connection")
async def api_test_database_connection(
wallet: WalletTypeInfo = Depends(require_admin_key),
):
"""Test connection to Lamassu database with detailed reporting"""
try:
from .transaction_processor import transaction_processor
# Use the detailed test method
result = await transaction_processor.test_connection_detailed()
return result
except Exception as e:
return {
"success": False,
"message": f"Test connection error: {str(e)}",
"steps": [f"❌ Unexpected error: {str(e)}"],
"ssh_tunnel_used": False,
"ssh_tunnel_success": False,
"database_connection_success": False
}
@satmachineclient_api_router.post("/api/v1/dca/manual-poll")
async def api_manual_poll(
wallet: WalletTypeInfo = Depends(require_admin_key),
):
"""Manually trigger a poll of the Lamassu database"""
try:
from .transaction_processor import transaction_processor
from .crud import update_poll_start_time, update_poll_success_time
# 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"
)
config_id = db_config["config_id"]
# Record manual poll start time
await update_poll_start_time(config_id)
# Fetch and process transactions via SSH
new_transactions = await transaction_processor.fetch_new_transactions(db_config)
transactions_processed = 0
for transaction in new_transactions:
await transaction_processor.process_transaction(transaction)
transactions_processed += 1
# Record successful manual poll completion
await update_poll_success_time(config_id)
return {
"success": True,
"transactions_processed": transactions_processed,
"message": f"Processed {transactions_processed} new transactions since last poll"
}
except Exception as e:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail=f"Error during manual poll: {str(e)}"
)
@satmachineclient_api_router.post("/api/v1/dca/test-transaction")
async def api_test_transaction(
wallet: WalletTypeInfo = Depends(require_admin_key),
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
@satmachineclient_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()
@satmachineclient_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
@satmachineclient_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
@satmachineclient_api_router.get("/api/v1/dca/config")
async def api_get_lamassu_config(
wallet: WalletTypeInfo = Depends(require_invoice_key),
) -> Optional[LamassuConfig]:
"""Get active Lamassu database configuration"""
return await get_active_lamassu_config()
@satmachineclient_api_router.post("/api/v1/dca/config", status_code=HTTPStatus.CREATED)
async def api_create_lamassu_config(
data: CreateLamassuConfigData,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> LamassuConfig:
"""Create/update Lamassu database configuration"""
return await create_lamassu_config(data)
@satmachineclient_api_router.put("/api/v1/dca/config/{config_id}")
async def api_update_lamassu_config(
config_id: str,
data: UpdateLamassuConfigData,
wallet: WalletTypeInfo = Depends(require_admin_key),
) -> LamassuConfig:
"""Update Lamassu database configuration"""
config = await get_lamassu_config(config_id)
if not config:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Configuration not found."
)
updated_config = await update_lamassu_config(config_id, data)
if not updated_config:
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Failed to update configuration."
)
return updated_config
@satmachineclient_api_router.delete("/api/v1/dca/config/{config_id}")
async def api_delete_lamassu_config(
config_id: str,
wallet: WalletTypeInfo = Depends(require_admin_key),
):
"""Delete Lamassu database configuration"""
config = await get_lamassu_config(config_id)
if not config:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Configuration not found."
)
await delete_lamassu_config(config_id)
return {"message": "Configuration deleted successfully"}