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.db import Database
|
||||||
from lnbits.helpers import urlsafe_short_hash
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
|
||||||
from .models import (
|
from .models import ()
|
||||||
CreateDcaClientData, DcaClient, UpdateDcaClientData,
|
|
||||||
CreateDepositData, DcaDeposit, UpdateDepositStatusData,
|
|
||||||
CreateDcaPaymentData, DcaPayment,
|
|
||||||
ClientBalanceSummary,
|
|
||||||
CreateLamassuConfigData, LamassuConfig, UpdateLamassuConfigData,
|
|
||||||
CreateLamassuTransactionData, StoredLamassuTransaction
|
|
||||||
)
|
|
||||||
|
|
||||||
db = Database("ext_satmachineclient")
|
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]:
|
async def get_dca_client(client_id: str) -> Optional[DcaClient]:
|
||||||
return await db.fetchone(
|
return await db.fetchone(
|
||||||
"SELECT * FROM satmachineclient.dca_clients WHERE id = :id",
|
"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]:
|
async def get_dca_client_by_user(user_id: str) -> Optional[DcaClient]:
|
||||||
return await db.fetchone(
|
return await db.fetchone(
|
||||||
"SELECT * FROM satmachineclient.dca_clients WHERE user_id = :user_id",
|
"SELECT * FROM satmachineclient.dca_clients WHERE user_id = :user_id",
|
||||||
|
|
|
||||||
|
|
@ -4,122 +4,6 @@ window.app = Vue.createApp({
|
||||||
delimiters: ['${', '}'],
|
delimiters: ['${', '}'],
|
||||||
data: function () {
|
data: function () {
|
||||||
return {
|
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'
|
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/////
|
//////LIFECYCLE FUNCTIONS RUNNING ON PAGE LOAD/////
|
||||||
///////////////////////////////////////////////////
|
///////////////////////////////////////////////////
|
||||||
async created() {
|
async created() {
|
||||||
// Load DCA admin data
|
// Load DCA client data
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this.getLamassuConfig(),
|
|
||||||
this.getDcaClients(),
|
|
||||||
this.getDeposits(),
|
|
||||||
this.getLamassuTransactions()
|
|
||||||
])
|
])
|
||||||
},
|
},
|
||||||
|
|
||||||
computed: {
|
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>
|
<script src="{{ static_url_for('satmachineclient/static', path='js/index.js') }}"></script>
|
||||||
{% endblock %} {% block page %}
|
{% endblock %} {% block page %}
|
||||||
<div class="row q-col-gutter-md" id="dcaClient">
|
<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>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
347
views_api.py
347
views_api.py
|
|
@ -7,40 +7,23 @@ from fastapi import APIRouter, Depends, Request
|
||||||
from lnbits.core.crud import get_user
|
from lnbits.core.crud import get_user
|
||||||
from lnbits.core.models import WalletTypeInfo
|
from lnbits.core.models import WalletTypeInfo
|
||||||
from lnbits.core.services import create_invoice
|
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 starlette.exceptions import HTTPException
|
||||||
|
|
||||||
from .crud import (
|
from .crud import (
|
||||||
# DCA CRUD operations
|
# DCA CRUD operations
|
||||||
create_dca_client,
|
create_dca_client,
|
||||||
get_dca_clients,
|
|
||||||
get_dca_client,
|
get_dca_client,
|
||||||
update_dca_client,
|
|
||||||
delete_dca_client,
|
|
||||||
create_deposit,
|
|
||||||
get_all_deposits,
|
get_all_deposits,
|
||||||
get_deposit,
|
get_deposit,
|
||||||
update_deposit_status,
|
|
||||||
get_client_balance_summary,
|
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 (
|
from .models import (
|
||||||
# DCA models
|
# DCA models
|
||||||
CreateDcaClientData, DcaClient, UpdateDcaClientData,
|
CreateDcaClientData,
|
||||||
CreateDepositData, DcaDeposit, UpdateDepositStatusData,
|
DcaClient,
|
||||||
|
DcaDeposit,
|
||||||
ClientBalanceSummary,
|
ClientBalanceSummary,
|
||||||
CreateLamassuConfigData, LamassuConfig, UpdateLamassuConfigData,
|
|
||||||
StoredLamassuTransaction
|
|
||||||
)
|
)
|
||||||
|
|
||||||
satmachineclient_api_router = APIRouter()
|
satmachineclient_api_router = APIRouter()
|
||||||
|
|
@ -51,46 +34,21 @@ satmachineclient_api_router = APIRouter()
|
||||||
###################################################
|
###################################################
|
||||||
|
|
||||||
# DCA Client Endpoints
|
# DCA Client Endpoints
|
||||||
|
# Note: Client creation/update
|
||||||
@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()
|
|
||||||
|
|
||||||
|
|
||||||
@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)
|
@satmachineclient_api_router.post("/api/v1/dca/clients", status_code=HTTPStatus.CREATED)
|
||||||
async def api_create_test_dca_client(
|
async def api_create_test_dca_client(
|
||||||
data: CreateDcaClientData,
|
data: CreateDcaClientData,
|
||||||
wallet: WalletTypeInfo = Depends(require_admin_key),
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
) -> DcaClient:
|
) -> DcaClient:
|
||||||
"""Create a test DCA client (temporary for testing)"""
|
|
||||||
return await create_dca_client(data)
|
return await create_dca_client(data)
|
||||||
|
|
||||||
|
|
||||||
@satmachineclient_api_router.get("/api/v1/dca/clients/{client_id}/balance")
|
@satmachineclient_api_router.get("/api/v1/dca/clients/{client_id}/balance")
|
||||||
async def api_get_client_balance(
|
async def api_get_client_balance(
|
||||||
client_id: str,
|
client_id: str,
|
||||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
) -> ClientBalanceSummary:
|
) -> ClientBalanceSummary:
|
||||||
"""Get client balance summary"""
|
"""Get client balance summary"""
|
||||||
client = await get_dca_client(client_id)
|
client = await get_dca_client(client_id)
|
||||||
|
|
@ -98,24 +56,27 @@ async def api_get_client_balance(
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="DCA client not found."
|
status_code=HTTPStatus.NOT_FOUND, detail="DCA client not found."
|
||||||
)
|
)
|
||||||
|
|
||||||
return await get_client_balance_summary(client_id)
|
return await get_client_balance_summary(client_id)
|
||||||
|
|
||||||
|
|
||||||
# DCA Deposit Endpoints
|
# 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")
|
@satmachineclient_api_router.get("/api/v1/dca/deposits")
|
||||||
async def api_get_deposits(
|
async def api_get_deposits(
|
||||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
) -> list[DcaDeposit]:
|
) -> list[DcaDeposit]:
|
||||||
"""Get all deposits"""
|
"""Get all deposits"""
|
||||||
return await 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}")
|
@satmachineclient_api_router.get("/api/v1/dca/deposits/{deposit_id}")
|
||||||
async def api_get_deposit(
|
async def api_get_deposit(
|
||||||
deposit_id: str,
|
deposit_id: str,
|
||||||
wallet: WalletTypeInfo = Depends(require_invoice_key),
|
wallet: WalletTypeInfo = Depends(require_admin_key),
|
||||||
) -> DcaDeposit:
|
) -> DcaDeposit:
|
||||||
"""Get a specific deposit"""
|
"""Get a specific deposit"""
|
||||||
deposit = await get_deposit(deposit_id)
|
deposit = await get_deposit(deposit_id)
|
||||||
|
|
@ -124,287 +85,3 @@ async def api_get_deposit(
|
||||||
status_code=HTTPStatus.NOT_FOUND, detail="Deposit not found."
|
status_code=HTTPStatus.NOT_FOUND, detail="Deposit not found."
|
||||||
)
|
)
|
||||||
return deposit
|
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