Clear out
This commit is contained in:
parent
462c93d18d
commit
8328861e22
4 changed files with 14 additions and 1769 deletions
40
crud.py
40
crud.py
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
345
views_api.py
345
views_api.py
|
|
@ -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"}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue