Compare commits
10 commits
aa71321c84
...
cd0d958c2c
| Author | SHA1 | Date | |
|---|---|---|---|
| cd0d958c2c | |||
| 1b7374fa70 | |||
| fe38e08d4e | |||
| 230beccc37 | |||
| 077e097fc2 | |||
| 4843b43147 | |||
| 5d41e0c50e | |||
| bca39b91cd | |||
| a864f285e4 | |||
| c83ebf43ab |
11 changed files with 3197 additions and 247 deletions
2
crud.py
2
crud.py
|
|
@ -267,7 +267,7 @@ async def get_client_balance_summary(client_id: str, as_of_time: Optional[dateti
|
|||
from loguru import logger
|
||||
# Verify timezone consistency for temporal filtering
|
||||
tz_info = "UTC" if as_of_time.tzinfo == timezone.utc else f"TZ: {as_of_time.tzinfo}"
|
||||
logger.info(f"Client {client_id[:8]}... balance as of {as_of_time} ({tz_info}): deposits.confirmed_at <= cutoff, payments.transaction_time <= cutoff → {total_deposits - total_payments} centavos remaining")
|
||||
logger.info(f"Client {client_id[:8]}... balance as of {as_of_time} ({tz_info}): deposits.confirmed_at <= cutoff, payments.transaction_time <= cutoff → {total_deposits - total_payments:.2f} GTQ remaining")
|
||||
|
||||
return ClientBalanceSummary(
|
||||
client_id=client_id,
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
# Currency conversion utilities for API boundary
|
||||
from decimal import Decimal
|
||||
from typing import Union
|
||||
|
||||
|
||||
def gtq_to_centavos(gtq_amount: Union[float, int, str]) -> int:
|
||||
"""Convert GTQ to centavos for database storage"""
|
||||
return int(Decimal(str(gtq_amount)) * 100)
|
||||
|
||||
|
||||
def centavos_to_gtq(centavos: int) -> float:
|
||||
"""Convert centavos to GTQ for API responses"""
|
||||
return float(centavos) / 100
|
||||
|
||||
|
||||
def format_gtq_currency(centavos: int) -> str:
|
||||
"""Format centavos as GTQ currency string"""
|
||||
gtq_amount = centavos_to_gtq(centavos)
|
||||
return f"Q{gtq_amount:.2f}"
|
||||
|
||||
|
||||
# Conversion helpers for API responses
|
||||
def deposit_db_to_api(deposit_db) -> dict:
|
||||
"""Convert database deposit model to API response"""
|
||||
return {
|
||||
"id": deposit_db.id,
|
||||
"client_id": deposit_db.client_id,
|
||||
"amount_gtq": centavos_to_gtq(deposit_db.amount),
|
||||
"currency": deposit_db.currency,
|
||||
"status": deposit_db.status,
|
||||
"notes": deposit_db.notes,
|
||||
"created_at": deposit_db.created_at,
|
||||
"confirmed_at": deposit_db.confirmed_at
|
||||
}
|
||||
|
||||
|
||||
def balance_summary_db_to_api(balance_db) -> dict:
|
||||
"""Convert database balance summary to API response"""
|
||||
return {
|
||||
"client_id": balance_db.client_id,
|
||||
"total_deposits_gtq": centavos_to_gtq(balance_db.total_deposits),
|
||||
"total_payments_gtq": centavos_to_gtq(balance_db.total_payments),
|
||||
"remaining_balance_gtq": centavos_to_gtq(balance_db.remaining_balance),
|
||||
"currency": balance_db.currency
|
||||
}
|
||||
|
|
@ -135,3 +135,38 @@ async def m003_add_max_daily_limit_config(db):
|
|||
ADD COLUMN max_daily_limit_gtq INTEGER NOT NULL DEFAULT 2000
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
async def m004_convert_to_gtq_storage(db):
|
||||
"""
|
||||
Convert centavo storage to GTQ storage by changing data types and converting existing data.
|
||||
Handles both SQLite (data conversion only) and PostgreSQL (data + schema changes).
|
||||
"""
|
||||
# Detect database type
|
||||
db_type = str(type(db)).lower()
|
||||
is_postgres = 'postgres' in db_type or 'asyncpg' in db_type
|
||||
|
||||
if is_postgres:
|
||||
# PostgreSQL: Need to change column types first, then convert data
|
||||
|
||||
# Change column types to DECIMAL(10,2)
|
||||
await db.execute("ALTER TABLE satoshimachine.dca_deposits ALTER COLUMN amount TYPE DECIMAL(10,2)")
|
||||
await db.execute("ALTER TABLE satoshimachine.dca_payments ALTER COLUMN amount_fiat TYPE DECIMAL(10,2)")
|
||||
await db.execute("ALTER TABLE satoshimachine.lamassu_transactions ALTER COLUMN fiat_amount TYPE DECIMAL(10,2)")
|
||||
await db.execute("ALTER TABLE satoshimachine.dca_clients ALTER COLUMN fixed_mode_daily_limit TYPE DECIMAL(10,2)")
|
||||
await db.execute("ALTER TABLE satoshimachine.lamassu_config ALTER COLUMN max_daily_limit_gtq TYPE DECIMAL(10,2)")
|
||||
|
||||
# Convert data from centavos to GTQ
|
||||
await db.execute("UPDATE satoshimachine.dca_deposits SET amount = amount / 100.0 WHERE currency = 'GTQ'")
|
||||
await db.execute("UPDATE satoshimachine.dca_payments SET amount_fiat = amount_fiat / 100.0")
|
||||
await db.execute("UPDATE satoshimachine.lamassu_transactions SET fiat_amount = fiat_amount / 100.0")
|
||||
await db.execute("UPDATE satoshimachine.dca_clients SET fixed_mode_daily_limit = fixed_mode_daily_limit / 100.0 WHERE fixed_mode_daily_limit IS NOT NULL")
|
||||
await db.execute("UPDATE satoshimachine.lamassu_config SET max_daily_limit_gtq = max_daily_limit_gtq / 100.0 WHERE max_daily_limit_gtq > 1000")
|
||||
|
||||
else:
|
||||
# SQLite: Data conversion only (dynamic typing handles the rest)
|
||||
await db.execute("UPDATE satoshimachine.dca_deposits SET amount = CAST(amount AS REAL) / 100.0 WHERE currency = 'GTQ'")
|
||||
await db.execute("UPDATE satoshimachine.dca_payments SET amount_fiat = CAST(amount_fiat AS REAL) / 100.0")
|
||||
await db.execute("UPDATE satoshimachine.lamassu_transactions SET fiat_amount = CAST(fiat_amount AS REAL) / 100.0")
|
||||
await db.execute("UPDATE satoshimachine.dca_clients SET fixed_mode_daily_limit = CAST(fixed_mode_daily_limit AS REAL) / 100.0 WHERE fixed_mode_daily_limit IS NOT NULL")
|
||||
await db.execute("UPDATE satoshimachine.lamassu_config SET max_daily_limit_gtq = CAST(max_daily_limit_gtq AS REAL) / 100.0 WHERE max_daily_limit_gtq > 1000")
|
||||
1362
misc-docs/EMERGENCY_PROTOCOLS.md
Normal file
1362
misc-docs/EMERGENCY_PROTOCOLS.md
Normal file
File diff suppressed because it is too large
Load diff
1362
misc-docs/EMERGENCY_PROTOCOLS_PRINT.md
Normal file
1362
misc-docs/EMERGENCY_PROTOCOLS_PRINT.md
Normal file
File diff suppressed because it is too large
Load diff
82
models.py
82
models.py
|
|
@ -3,7 +3,7 @@
|
|||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, validator
|
||||
|
||||
|
||||
# DCA Client Models
|
||||
|
|
@ -12,7 +12,7 @@ class CreateDcaClientData(BaseModel):
|
|||
wallet_id: str
|
||||
username: str
|
||||
dca_mode: str = "flow" # 'flow' or 'fixed'
|
||||
fixed_mode_daily_limit: Optional[int] = None
|
||||
fixed_mode_daily_limit: Optional[float] = None
|
||||
|
||||
|
||||
class DcaClient(BaseModel):
|
||||
|
|
@ -30,45 +30,29 @@ class DcaClient(BaseModel):
|
|||
class UpdateDcaClientData(BaseModel):
|
||||
username: Optional[str] = None
|
||||
dca_mode: Optional[str] = None
|
||||
fixed_mode_daily_limit: Optional[int] = None
|
||||
fixed_mode_daily_limit: Optional[float] = None
|
||||
status: Optional[str] = None
|
||||
|
||||
|
||||
# API Models for Deposits (Frontend <-> Backend communication)
|
||||
class CreateDepositAPI(BaseModel):
|
||||
"""API model - frontend sends GTQ amounts"""
|
||||
client_id: str
|
||||
amount_gtq: float # Amount in GTQ (e.g., 150.75)
|
||||
currency: str = "GTQ"
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
class DepositAPI(BaseModel):
|
||||
"""API model - backend returns GTQ amounts"""
|
||||
id: str
|
||||
client_id: str
|
||||
amount_gtq: float # Amount in GTQ (e.g., 150.75)
|
||||
currency: str
|
||||
status: str # 'pending' or 'confirmed'
|
||||
notes: Optional[str]
|
||||
created_at: datetime
|
||||
confirmed_at: Optional[datetime]
|
||||
|
||||
|
||||
# Database Models for Deposits (Internal storage in centavos)
|
||||
# Deposit Models (Now storing GTQ directly)
|
||||
class CreateDepositData(BaseModel):
|
||||
"""Internal model - database stores centavos"""
|
||||
client_id: str
|
||||
amount: int # Amount in smallest currency unit (centavos for GTQ)
|
||||
amount: float # Amount in GTQ (e.g., 150.75)
|
||||
currency: str = "GTQ"
|
||||
notes: Optional[str] = None
|
||||
|
||||
@validator('amount')
|
||||
def round_amount_to_cents(cls, v):
|
||||
"""Ensure amount is rounded to 2 decimal places for DECIMAL(10,2) storage"""
|
||||
if v is not None:
|
||||
return round(float(v), 2)
|
||||
return v
|
||||
|
||||
|
||||
class DcaDeposit(BaseModel):
|
||||
"""Internal model - database stores centavos"""
|
||||
id: str
|
||||
client_id: str
|
||||
amount: int
|
||||
amount: float # Amount in GTQ (e.g., 150.75)
|
||||
currency: str
|
||||
status: str # 'pending' or 'confirmed'
|
||||
notes: Optional[str]
|
||||
|
|
@ -85,7 +69,7 @@ class UpdateDepositStatusData(BaseModel):
|
|||
class CreateDcaPaymentData(BaseModel):
|
||||
client_id: str
|
||||
amount_sats: int
|
||||
amount_fiat: int # Stored in centavos (GTQ * 100) for precision
|
||||
amount_fiat: float # Amount in GTQ (e.g., 150.75)
|
||||
exchange_rate: float
|
||||
transaction_type: str # 'flow', 'fixed', 'manual', 'commission'
|
||||
lamassu_transaction_id: Optional[str] = None
|
||||
|
|
@ -97,7 +81,7 @@ class DcaPayment(BaseModel):
|
|||
id: str
|
||||
client_id: str
|
||||
amount_sats: int
|
||||
amount_fiat: int # Stored in centavos (GTQ * 100) for precision
|
||||
amount_fiat: float # Amount in GTQ (e.g., 150.75)
|
||||
exchange_rate: float
|
||||
transaction_type: str
|
||||
lamassu_transaction_id: Optional[str]
|
||||
|
|
@ -107,30 +91,19 @@ class DcaPayment(BaseModel):
|
|||
transaction_time: Optional[datetime] = None # Original ATM transaction time
|
||||
|
||||
|
||||
# API Models for Client Balance Summary
|
||||
class ClientBalanceSummaryAPI(BaseModel):
|
||||
"""API model - returns GTQ amounts"""
|
||||
client_id: str
|
||||
total_deposits_gtq: float # Total confirmed deposits in GTQ
|
||||
total_payments_gtq: float # Total payments made in GTQ
|
||||
remaining_balance_gtq: float # Available balance for DCA in GTQ
|
||||
currency: str
|
||||
|
||||
|
||||
# Internal Models for Client Balance Summary
|
||||
# Client Balance Summary (Now storing GTQ directly)
|
||||
class ClientBalanceSummary(BaseModel):
|
||||
"""Internal model - stores centavos"""
|
||||
client_id: str
|
||||
total_deposits: int # Total confirmed deposits
|
||||
total_payments: int # Total payments made
|
||||
remaining_balance: int # Available balance for DCA
|
||||
total_deposits: float # Total confirmed deposits in GTQ
|
||||
total_payments: float # Total payments made in GTQ
|
||||
remaining_balance: float # Available balance for DCA in GTQ
|
||||
currency: str
|
||||
|
||||
|
||||
# Transaction Processing Models
|
||||
class LamassuTransaction(BaseModel):
|
||||
transaction_id: str
|
||||
amount_fiat: int # Stored in centavos (GTQ * 100) for precision
|
||||
amount_fiat: float # Amount in GTQ (e.g., 150.75)
|
||||
amount_crypto: int
|
||||
exchange_rate: float
|
||||
transaction_type: str # 'cash_in' or 'cash_out'
|
||||
|
|
@ -141,7 +114,7 @@ class LamassuTransaction(BaseModel):
|
|||
# Lamassu Transaction Storage Models
|
||||
class CreateLamassuTransactionData(BaseModel):
|
||||
lamassu_transaction_id: str
|
||||
fiat_amount: int # Stored in centavos (GTQ * 100) for precision
|
||||
fiat_amount: float # Amount in GTQ (e.g., 150.75)
|
||||
crypto_amount: int
|
||||
commission_percentage: float
|
||||
discount: float = 0.0
|
||||
|
|
@ -158,7 +131,7 @@ class CreateLamassuTransactionData(BaseModel):
|
|||
class StoredLamassuTransaction(BaseModel):
|
||||
id: str
|
||||
lamassu_transaction_id: str
|
||||
fiat_amount: int
|
||||
fiat_amount: float # Amount in GTQ (e.g., 150.75)
|
||||
crypto_amount: int
|
||||
commission_percentage: float
|
||||
discount: float
|
||||
|
|
@ -194,7 +167,14 @@ class CreateLamassuConfigData(BaseModel):
|
|||
ssh_password: Optional[str] = None
|
||||
ssh_private_key: Optional[str] = None # Path to private key file or key content
|
||||
# DCA Client Limits
|
||||
max_daily_limit_gtq: int = 2000 # Maximum daily limit for Fixed mode clients
|
||||
max_daily_limit_gtq: float = 2000.0 # Maximum daily limit for Fixed mode clients
|
||||
|
||||
@validator('max_daily_limit_gtq')
|
||||
def round_max_daily_limit(cls, v):
|
||||
"""Ensure max daily limit is rounded to 2 decimal places"""
|
||||
if v is not None:
|
||||
return round(float(v), 2)
|
||||
return v
|
||||
|
||||
|
||||
class LamassuConfig(BaseModel):
|
||||
|
|
@ -224,7 +204,7 @@ class LamassuConfig(BaseModel):
|
|||
last_poll_time: Optional[datetime] = None
|
||||
last_successful_poll: Optional[datetime] = None
|
||||
# DCA Client Limits
|
||||
max_daily_limit_gtq: int = 2000 # Maximum daily limit for Fixed mode clients
|
||||
max_daily_limit_gtq: float = 2000.0 # Maximum daily limit for Fixed mode clients
|
||||
|
||||
|
||||
class UpdateLamassuConfigData(BaseModel):
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ window.app = Vue.createApp({
|
|||
depositsTable: {
|
||||
columns: [
|
||||
{ name: 'client_id', align: 'left', label: 'Client', field: 'client_id' },
|
||||
{ name: 'amount_gtq', align: 'left', label: 'Amount', field: 'amount_gtq' },
|
||||
{ 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' },
|
||||
|
|
@ -92,8 +92,15 @@ window.app = Vue.createApp({
|
|||
testingConnection: false,
|
||||
runningManualPoll: false,
|
||||
runningTestTransaction: false,
|
||||
processingSpecificTransaction: false,
|
||||
lamassuConfig: null,
|
||||
|
||||
// Manual transaction processing
|
||||
manualTransactionDialog: {
|
||||
show: false,
|
||||
transactionId: ''
|
||||
},
|
||||
|
||||
// Config dialog
|
||||
configDialog: {
|
||||
show: false,
|
||||
|
|
@ -130,11 +137,11 @@ window.app = Vue.createApp({
|
|||
///////////////////////////////////////////////////
|
||||
|
||||
methods: {
|
||||
// Utility Methods - Simplified since API handles conversion
|
||||
// Utility Methods
|
||||
formatCurrency(amount) {
|
||||
if (!amount) return 'Q 0.00';
|
||||
|
||||
// Amount is already in GTQ from API
|
||||
// Amount is now stored as GTQ directly in database
|
||||
return new Intl.NumberFormat('es-GT', {
|
||||
style: 'currency',
|
||||
currency: 'GTQ',
|
||||
|
|
@ -279,7 +286,7 @@ window.app = Vue.createApp({
|
|||
)
|
||||
return {
|
||||
...client,
|
||||
remaining_balance: balance.remaining_balance_gtq
|
||||
remaining_balance: balance.remaining_balance
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error fetching balance for client ${client.id}:`, error)
|
||||
|
|
@ -303,7 +310,7 @@ window.app = Vue.createApp({
|
|||
try {
|
||||
const data = {
|
||||
client_id: this.quickDepositForm.selectedClient?.value,
|
||||
amount_gtq: this.quickDepositForm.amount, // Send GTQ directly - API handles conversion
|
||||
amount: this.quickDepositForm.amount, // Send GTQ directly - now stored as GTQ
|
||||
currency: 'GTQ',
|
||||
notes: this.quickDepositForm.notes
|
||||
}
|
||||
|
|
@ -376,7 +383,7 @@ window.app = Vue.createApp({
|
|||
try {
|
||||
const data = {
|
||||
client_id: this.depositFormDialog.data.client_id,
|
||||
amount_gtq: this.depositFormDialog.data.amount, // Send GTQ directly - API handles conversion
|
||||
amount: this.depositFormDialog.data.amount, // Send GTQ directly - now stored as GTQ
|
||||
currency: this.depositFormDialog.data.currency,
|
||||
notes: this.depositFormDialog.data.notes
|
||||
}
|
||||
|
|
@ -586,6 +593,79 @@ window.app = Vue.createApp({
|
|||
await this.getDeposits()
|
||||
await this.getLamassuTransactions()
|
||||
await this.getLamassuConfig()
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
} finally {
|
||||
this.runningTestTransaction = false
|
||||
}
|
||||
},
|
||||
|
||||
openManualTransactionDialog() {
|
||||
this.manualTransactionDialog.transactionId = ''
|
||||
this.manualTransactionDialog.show = true
|
||||
},
|
||||
|
||||
async processSpecificTransaction() {
|
||||
if (!this.manualTransactionDialog.transactionId) {
|
||||
this.$q.notify({
|
||||
type: 'warning',
|
||||
message: 'Please enter a transaction ID',
|
||||
timeout: 3000
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.processingSpecificTransaction = true
|
||||
try {
|
||||
const { data } = await LNbits.api.request(
|
||||
'POST',
|
||||
`/satmachineadmin/api/v1/dca/process-transaction/${this.manualTransactionDialog.transactionId}`,
|
||||
null
|
||||
)
|
||||
|
||||
if (data.already_processed) {
|
||||
this.$q.notify({
|
||||
type: 'warning',
|
||||
message: `Transaction already processed with ${data.payment_count} distributions`,
|
||||
timeout: 5000
|
||||
})
|
||||
this.manualTransactionDialog.show = false
|
||||
return
|
||||
}
|
||||
|
||||
// Show detailed results
|
||||
const details = data.transaction_details
|
||||
let dialogContent = `<strong>Manual Transaction Processing Results</strong><br/><br/>`
|
||||
dialogContent += `<strong>Transaction ID:</strong> ${details.transaction_id}<br/>`
|
||||
dialogContent += `<strong>Status:</strong> ${details.status}<br/>`
|
||||
dialogContent += `<strong>Dispense:</strong> ${details.dispense ? 'Yes' : 'No'}<br/>`
|
||||
dialogContent += `<strong>Dispense Confirmed:</strong> ${details.dispense_confirmed ? 'Yes' : 'No'}<br/>`
|
||||
dialogContent += `<strong>Crypto Amount:</strong> ${details.crypto_amount} sats<br/>`
|
||||
dialogContent += `<strong>Fiat Amount:</strong> ${details.fiat_amount}<br/>`
|
||||
dialogContent += `<br/><strong>Transaction processed successfully!</strong>`
|
||||
|
||||
this.$q.dialog({
|
||||
title: 'Transaction Processed',
|
||||
message: dialogContent,
|
||||
html: true,
|
||||
ok: {
|
||||
color: 'positive',
|
||||
label: 'Great!'
|
||||
}
|
||||
})
|
||||
|
||||
this.$q.notify({
|
||||
type: 'positive',
|
||||
message: `Transaction ${details.transaction_id} processed successfully`,
|
||||
timeout: 5000
|
||||
})
|
||||
|
||||
// Close dialog and refresh data
|
||||
this.manualTransactionDialog.show = false
|
||||
await this.getDcaClients()
|
||||
await this.getDeposits()
|
||||
await this.getLamassuTransactions()
|
||||
await this.getLamassuConfig()
|
||||
|
||||
} catch (error) {
|
||||
LNbits.utils.notifyApiError(error)
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@
|
|||
<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_gtq'">${ formatCurrency(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 }
|
||||
|
|
@ -346,14 +346,15 @@
|
|||
Manual Poll
|
||||
</q-btn>
|
||||
<q-btn
|
||||
v-if="lamassuConfig && lamassuConfig.source_wallet_id"
|
||||
v-if="lamassuConfig"
|
||||
size="sm"
|
||||
color="warning"
|
||||
@click="testTransaction"
|
||||
:loading="runningTestTransaction"
|
||||
color="deep-orange"
|
||||
@click="openManualTransactionDialog"
|
||||
class="q-ml-sm"
|
||||
icon="build"
|
||||
>
|
||||
Test Transaction
|
||||
<q-tooltip>Process specific transaction by ID (bypasses dispense checks)</q-tooltip>
|
||||
Manual TX
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
|
@ -470,9 +471,9 @@
|
|||
<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_gtq) } |
|
||||
Payments: ${ formatCurrency(clientDetailsDialog.balance.total_payments_gtq) } |
|
||||
Remaining: ${ formatCurrency(clientDetailsDialog.balance.remaining_balance_gtq) }
|
||||
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>
|
||||
|
|
@ -778,5 +779,64 @@
|
|||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
<!--/////////////////////////////////////////////////-->
|
||||
<!--//////////////MANUAL TRANSACTION DIALOG///////////-->
|
||||
<!--/////////////////////////////////////////////////-->
|
||||
|
||||
<q-dialog v-model="manualTransactionDialog.show" position="top">
|
||||
<q-card class="q-pa-lg q-pt-xl" style="width: 500px; max-width: 90vw">
|
||||
<div class="text-h6 q-mb-md">Process Specific Transaction</div>
|
||||
|
||||
<q-banner class="bg-orange-1 text-orange-9 q-mb-md">
|
||||
<template v-slot:avatar>
|
||||
<q-icon name="warning" color="orange" />
|
||||
</template>
|
||||
<div class="text-caption">
|
||||
<strong>Use with caution:</strong> This bypasses all dispense status checks and will process the transaction even if dispense_confirmed is false. Only use this for manually settled transactions.
|
||||
</div>
|
||||
</q-banner>
|
||||
|
||||
<q-form @submit="processSpecificTransaction" class="q-gutter-md">
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
v-model.trim="manualTransactionDialog.transactionId"
|
||||
label="Lamassu Transaction ID *"
|
||||
placeholder="e.g., 82746dfb-674d-4d7e-b008-7507caa02773"
|
||||
hint="Enter the transaction/session ID from Lamassu database"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<q-icon name="receipt" />
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<div class="text-caption text-grey-7">
|
||||
This will:
|
||||
<ul class="q-my-sm">
|
||||
<li>Fetch the transaction from Lamassu regardless of dispense status</li>
|
||||
<li>Process it through the normal DCA distribution flow</li>
|
||||
<li>Credit the source wallet and distribute to clients</li>
|
||||
<li>Send commission to the commission wallet (if configured)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="row q-mt-lg">
|
||||
<q-btn
|
||||
unelevated
|
||||
color="deep-orange"
|
||||
type="submit"
|
||||
:loading="processingSpecificTransaction"
|
||||
:disable="!manualTransactionDialog.transactionId"
|
||||
>
|
||||
Process Transaction
|
||||
</q-btn>
|
||||
<q-btn v-close-popup flat color="grey" class="q-ml-auto">
|
||||
Cancel
|
||||
</q-btn>
|
||||
</div>
|
||||
</q-form>
|
||||
</q-card>
|
||||
</q-dialog>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -493,14 +493,26 @@ class LamassuTransactionProcessor:
|
|||
# Convert string values to appropriate types
|
||||
processed_row = {}
|
||||
for key, value in row.items():
|
||||
if value == '':
|
||||
processed_row[key] = None
|
||||
# Handle None/empty values consistently at data ingestion boundary
|
||||
if value == '' or value is None:
|
||||
if key in ['fiat_amount', 'crypto_amount']:
|
||||
processed_row[key] = 0 # Default numeric fields to 0
|
||||
elif key in ['commission_percentage', 'discount']:
|
||||
processed_row[key] = 0.0 # Default percentage fields to 0.0
|
||||
else:
|
||||
processed_row[key] = None # Keep None for non-numeric fields
|
||||
elif key in ['transaction_id', 'device_id', 'crypto_code', 'fiat_code']:
|
||||
processed_row[key] = str(value)
|
||||
elif key in ['fiat_amount', 'crypto_amount']:
|
||||
processed_row[key] = int(float(value)) if value else 0
|
||||
try:
|
||||
processed_row[key] = int(float(value))
|
||||
except (ValueError, TypeError):
|
||||
processed_row[key] = 0 # Fallback to 0 for invalid values
|
||||
elif key in ['commission_percentage', 'discount']:
|
||||
processed_row[key] = float(value) if value else 0.0
|
||||
try:
|
||||
processed_row[key] = float(value)
|
||||
except (ValueError, TypeError):
|
||||
processed_row[key] = 0.0 # Fallback to 0.0 for invalid values
|
||||
elif key == 'transaction_time':
|
||||
from datetime import datetime
|
||||
# Parse PostgreSQL timestamp format and ensure it's in UTC for consistency
|
||||
|
|
@ -548,6 +560,44 @@ class LamassuTransactionProcessor:
|
|||
logger.error(f"Error executing SSH query: {e}")
|
||||
return []
|
||||
|
||||
async def fetch_transaction_by_id(self, db_config: Dict[str, Any], transaction_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Fetch a specific transaction by ID from Lamassu database, bypassing all status filters"""
|
||||
try:
|
||||
logger.info(f"Fetching transaction {transaction_id} from Lamassu database (bypass all filters)")
|
||||
|
||||
# Query for specific transaction ID without any status/dispense filters
|
||||
lamassu_query = f"""
|
||||
SELECT
|
||||
co.id as transaction_id,
|
||||
co.fiat as fiat_amount,
|
||||
co.crypto_atoms as crypto_amount,
|
||||
co.confirmed_at as transaction_time,
|
||||
co.device_id,
|
||||
co.status,
|
||||
co.commission_percentage,
|
||||
co.discount,
|
||||
co.crypto_code,
|
||||
co.fiat_code,
|
||||
co.dispense,
|
||||
co.dispense_confirmed
|
||||
FROM cash_out_txs co
|
||||
WHERE co.id = '{transaction_id}'
|
||||
"""
|
||||
|
||||
results = await self.execute_ssh_query(db_config, lamassu_query)
|
||||
|
||||
if not results:
|
||||
logger.warning(f"Transaction {transaction_id} not found in Lamassu database")
|
||||
return None
|
||||
|
||||
transaction = results[0]
|
||||
logger.info(f"Found transaction {transaction_id}: status={transaction.get('status')}, dispense={transaction.get('dispense')}, dispense_confirmed={transaction.get('dispense_confirmed')}")
|
||||
return transaction
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error fetching transaction {transaction_id} from Lamassu database: {e}")
|
||||
return None
|
||||
|
||||
async def fetch_new_transactions(self, db_config: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||
"""Fetch new successful transactions from Lamassu database since last poll"""
|
||||
try:
|
||||
|
|
@ -628,11 +678,11 @@ class LamassuTransactionProcessor:
|
|||
logger.info("No Flow Mode clients found - skipping distribution")
|
||||
return {}
|
||||
|
||||
# Extract transaction details with None-safe defaults
|
||||
crypto_atoms = transaction.get("crypto_amount") # Total sats with commission baked in
|
||||
fiat_amount = transaction.get("fiat_amount") # Actual fiat dispensed (principal only)
|
||||
commission_percentage = transaction.get("commission_percentage") # Already stored as decimal (e.g., 0.045)
|
||||
discount = transaction.get("discount") # Discount percentage
|
||||
# Extract transaction details - guaranteed clean from data ingestion
|
||||
crypto_atoms = transaction.get("crypto_amount", 0) # Total sats with commission baked in
|
||||
fiat_amount = transaction.get("fiat_amount", 0) # Actual fiat dispensed (principal only)
|
||||
commission_percentage = transaction.get("commission_percentage", 0.0) # Already stored as decimal (e.g., 0.045)
|
||||
discount = transaction.get("discount", 0.0) # Discount percentage
|
||||
transaction_time = transaction.get("transaction_time") # ATM transaction timestamp for temporal accuracy
|
||||
|
||||
# Normalize transaction_time to UTC if present
|
||||
|
|
@ -671,7 +721,7 @@ class LamassuTransactionProcessor:
|
|||
# Since crypto_atoms already includes commission, we need to extract the base amount
|
||||
# Formula: crypto_atoms = base_amount * (1 + effective_commission)
|
||||
# Therefore: base_amount = crypto_atoms / (1 + effective_commission)
|
||||
base_crypto_atoms = int(crypto_atoms / (1 + effective_commission))
|
||||
base_crypto_atoms = round(crypto_atoms / (1 + effective_commission))
|
||||
commission_amount_sats = crypto_atoms - base_crypto_atoms
|
||||
else:
|
||||
effective_commission = 0.0
|
||||
|
|
@ -696,9 +746,14 @@ class LamassuTransactionProcessor:
|
|||
for client in flow_clients:
|
||||
# Get balance as of the transaction time for temporal accuracy
|
||||
balance = await get_client_balance_summary(client.id, as_of_time=transaction_time)
|
||||
if balance.remaining_balance > 0: # Only include clients with remaining balance
|
||||
# Only include clients with positive remaining balance
|
||||
# NOTE: This works for fiat amounts that use cents
|
||||
if balance.remaining_balance >= 0.01:
|
||||
client_balances[client.id] = balance.remaining_balance
|
||||
total_confirmed_deposits += balance.remaining_balance
|
||||
logger.debug(f"Client {client.id[:8]}... included with balance: {balance.remaining_balance:.2f} GTQ")
|
||||
else:
|
||||
logger.info(f"Client {client.id[:8]}... excluded - zero/negative balance: {balance.remaining_balance:.2f} GTQ")
|
||||
|
||||
if total_confirmed_deposits == 0:
|
||||
logger.info("No clients with remaining DCA balance - skipping distribution")
|
||||
|
|
@ -755,9 +810,8 @@ class LamassuTransactionProcessor:
|
|||
client_sats_amount = calc['allocated_sats']
|
||||
proportion = calc['proportion']
|
||||
|
||||
# Calculate equivalent fiat value in centavos for tracking purposes (industry standard)
|
||||
# Store as centavos to maintain precision and avoid floating-point errors
|
||||
client_fiat_amount = round(client_sats_amount * 100 / exchange_rate) if exchange_rate > 0 else 0
|
||||
# Calculate equivalent fiat value in GTQ for tracking purposes
|
||||
client_fiat_amount = round(client_sats_amount / exchange_rate, 2) if exchange_rate > 0 else 0.0
|
||||
|
||||
distributions[client_id] = {
|
||||
"fiat_amount": client_fiat_amount,
|
||||
|
|
@ -765,7 +819,7 @@ class LamassuTransactionProcessor:
|
|||
"exchange_rate": exchange_rate
|
||||
}
|
||||
|
||||
logger.info(f"Client {client_id[:8]}... gets {client_sats_amount} sats (≈{client_fiat_amount/100:.2f} GTQ, {proportion:.2%} share)")
|
||||
logger.info(f"Client {client_id[:8]}... gets {client_sats_amount} sats (≈{client_fiat_amount:.2f} GTQ, {proportion:.2%} share)")
|
||||
|
||||
# Verification: ensure total distribution equals base amount
|
||||
total_distributed = sum(dist["sats_amount"] for dist in distributions.values())
|
||||
|
|
@ -781,9 +835,9 @@ class LamassuTransactionProcessor:
|
|||
current_balance = await get_client_balance_summary(client_id)
|
||||
if current_balance.remaining_balance > 0:
|
||||
final_distributions[client_id] = distribution
|
||||
logger.info(f"Client {client_id[:8]}... final balance check: {current_balance.remaining_balance} centavos - APPROVED for {distribution['sats_amount']} sats")
|
||||
logger.info(f"Client {client_id[:8]}... final balance check: {current_balance.remaining_balance:.2f} GTQ - APPROVED for {distribution['sats_amount']} sats")
|
||||
else:
|
||||
logger.warning(f"Client {client_id[:8]}... final balance check: {current_balance.remaining_balance} centavos - REJECTED (negative balance)")
|
||||
logger.warning(f"Client {client_id[:8]}... final balance check: {current_balance.remaining_balance:.2f} GTQ - REJECTED (negative balance)")
|
||||
|
||||
if len(final_distributions) != len(distributions):
|
||||
logger.warning(f"Rejected {len(distributions) - len(final_distributions)} clients due to negative balances during final check")
|
||||
|
|
@ -797,8 +851,8 @@ class LamassuTransactionProcessor:
|
|||
# In a production system, you might want to recalculate the entire distribution
|
||||
logger.warning("Proceeding with original distribution despite balance warnings - manual review recommended")
|
||||
|
||||
logger.info(f"Distribution verified: {total_distributed} sats distributed across {len(final_distributions)} clients")
|
||||
return final_distributions
|
||||
logger.info(f"Distribution verified: {total_distributed} sats distributed across {len(distributions)} clients (clients with positive allocations only)")
|
||||
return distributions
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error calculating distribution amounts: {e}")
|
||||
|
|
@ -830,22 +884,25 @@ class LamassuTransactionProcessor:
|
|||
# Final safety check: Verify client still has positive balance before payment
|
||||
current_balance = await get_client_balance_summary(client_id)
|
||||
if current_balance.remaining_balance <= 0:
|
||||
logger.error(f"CRITICAL: Client {client_id[:8]}... has negative balance ({current_balance.remaining_balance} centavos) - REFUSING payment of {distribution['sats_amount']} sats")
|
||||
logger.error(f"CRITICAL: Client {client_id[:8]}... has negative balance ({current_balance.remaining_balance:.2f} GTQ) - REFUSING payment of {distribution['sats_amount']} sats")
|
||||
continue
|
||||
|
||||
# Verify balance is sufficient for this distribution
|
||||
fiat_equivalent = distribution["fiat_amount"] # Already in centavos
|
||||
if current_balance.remaining_balance < fiat_equivalent:
|
||||
logger.error(f"CRITICAL: Client {client_id[:8]}... insufficient balance ({current_balance.remaining_balance} < {fiat_equivalent} centavos) - REFUSING payment")
|
||||
# Verify balance is sufficient for this distribution (round to 2 decimal places to match DECIMAL(10,2) precision)
|
||||
fiat_equivalent = distribution["fiat_amount"] # Amount in GTQ
|
||||
# Round both values to 2 decimal places to match database precision and avoid floating point comparison issues
|
||||
balance_rounded = round(current_balance.remaining_balance, 2)
|
||||
amount_rounded = round(fiat_equivalent, 2)
|
||||
if balance_rounded < amount_rounded:
|
||||
logger.error(f"CRITICAL: Client {client_id[:8]}... insufficient balance ({balance_rounded:.2f} < {amount_rounded:.2f} GTQ) - REFUSING payment")
|
||||
continue
|
||||
|
||||
logger.info(f"Client {client_id[:8]}... pre-payment balance check: {current_balance.remaining_balance} centavos - SUFFICIENT for {fiat_equivalent} centavos payment")
|
||||
logger.info(f"Client {client_id[:8]}... pre-payment balance check: {current_balance.remaining_balance:.2f} GTQ - SUFFICIENT for {fiat_equivalent:.2f} GTQ payment")
|
||||
|
||||
# Create DCA payment record
|
||||
payment_data = CreateDcaPaymentData(
|
||||
client_id=client_id,
|
||||
amount_sats=distribution["sats_amount"],
|
||||
amount_fiat=distribution["fiat_amount"], # Still store centavos in DB
|
||||
amount_fiat=distribution["fiat_amount"], # Amount in GTQ
|
||||
exchange_rate=distribution["exchange_rate"],
|
||||
transaction_type="flow",
|
||||
lamassu_transaction_id=transaction_id,
|
||||
|
|
@ -888,12 +945,9 @@ class LamassuTransactionProcessor:
|
|||
return False
|
||||
|
||||
# Create descriptive memo with DCA metrics
|
||||
fiat_amount_centavos = distribution.get("fiat_amount", 0)
|
||||
fiat_amount_gtq = distribution.get("fiat_amount", 0.0)
|
||||
exchange_rate = distribution.get("exchange_rate", 0)
|
||||
|
||||
# Convert centavos to GTQ for display
|
||||
fiat_amount_gtq = fiat_amount_centavos / 100
|
||||
|
||||
# Calculate cost basis (fiat per BTC)
|
||||
if exchange_rate > 0:
|
||||
# exchange_rate is sats per fiat unit, so convert to fiat per BTC
|
||||
|
|
@ -994,11 +1048,11 @@ class LamassuTransactionProcessor:
|
|||
async def store_lamassu_transaction(self, transaction: Dict[str, Any]) -> Optional[str]:
|
||||
"""Store the Lamassu transaction in our database for audit and UI"""
|
||||
try:
|
||||
# Extract and validate transaction data
|
||||
# Extract transaction data - guaranteed clean from data ingestion boundary
|
||||
crypto_atoms = transaction.get("crypto_amount", 0)
|
||||
fiat_amount = transaction.get("fiat_amount", 0)
|
||||
commission_percentage = transaction.get("commission_percentage") or 0.0
|
||||
discount = transaction.get("discount") or 0.0
|
||||
commission_percentage = transaction.get("commission_percentage", 0.0)
|
||||
discount = transaction.get("discount", 0.0)
|
||||
transaction_time = transaction.get("transaction_time")
|
||||
|
||||
# Normalize transaction_time to UTC if present
|
||||
|
|
@ -1011,7 +1065,7 @@ class LamassuTransactionProcessor:
|
|||
# Calculate commission metrics
|
||||
if commission_percentage > 0:
|
||||
effective_commission = commission_percentage * (100 - discount) / 100
|
||||
base_crypto_atoms = int(crypto_atoms / (1 + effective_commission))
|
||||
base_crypto_atoms = round(crypto_atoms / (1 + effective_commission))
|
||||
commission_amount_sats = crypto_atoms - base_crypto_atoms
|
||||
else:
|
||||
effective_commission = 0.0
|
||||
|
|
@ -1021,10 +1075,10 @@ class LamassuTransactionProcessor:
|
|||
# Calculate exchange rate
|
||||
exchange_rate = base_crypto_atoms / fiat_amount if fiat_amount > 0 else 0
|
||||
|
||||
# Create transaction data (store fiat_amount in centavos for consistency)
|
||||
# Create transaction data with GTQ amounts
|
||||
transaction_data = CreateLamassuTransactionData(
|
||||
lamassu_transaction_id=transaction["transaction_id"],
|
||||
fiat_amount=int(fiat_amount * 100), # Convert GTQ to centavos
|
||||
fiat_amount=round(fiat_amount, 2), # Store GTQ with 2 decimal places
|
||||
crypto_amount=crypto_atoms,
|
||||
commission_percentage=commission_percentage,
|
||||
discount=discount,
|
||||
|
|
@ -1065,7 +1119,19 @@ class LamassuTransactionProcessor:
|
|||
# Create invoice in commission wallet with DCA metrics
|
||||
fiat_amount = transaction.get("fiat_amount", 0)
|
||||
commission_percentage = transaction.get("commission_percentage", 0) * 100 # Convert to percentage
|
||||
commission_memo = f"DCA Commission: {commission_amount_sats:,} sats • {commission_percentage:.1f}% • {fiat_amount:,} GTQ transaction"
|
||||
discount = transaction.get("discount", 0.0) # Discount percentage
|
||||
|
||||
# Calculate effective commission for display
|
||||
if commission_percentage > 0:
|
||||
effective_commission_percentage = commission_percentage * (100 - discount) / 100
|
||||
else:
|
||||
effective_commission_percentage = 0.0
|
||||
|
||||
# Create detailed memo showing discount if applied
|
||||
if discount > 0:
|
||||
commission_memo = f"DCA Commission: {commission_amount_sats:,} sats • {commission_percentage:.1f}% - {discount:.1f}% discount = {effective_commission_percentage:.1f}% effective • {fiat_amount:,} GTQ transaction"
|
||||
else:
|
||||
commission_memo = f"DCA Commission: {commission_amount_sats:,} sats • {commission_percentage:.1f}% • {fiat_amount:,} GTQ transaction"
|
||||
|
||||
commission_payment = await create_invoice(
|
||||
wallet_id=admin_config.commission_wallet_id,
|
||||
|
|
@ -1132,12 +1198,12 @@ class LamassuTransactionProcessor:
|
|||
|
||||
# Calculate commission amount for sending to commission wallet
|
||||
crypto_atoms = transaction.get("crypto_amount", 0)
|
||||
commission_percentage = transaction.get("commission_percentage") or 0.0
|
||||
discount = transaction.get("discount") or 0.0
|
||||
commission_percentage = transaction.get("commission_percentage", 0.0)
|
||||
discount = transaction.get("discount", 0.0)
|
||||
|
||||
if commission_percentage and commission_percentage > 0:
|
||||
effective_commission = commission_percentage * (100 - discount) / 100
|
||||
base_crypto_atoms = int(crypto_atoms / (1 + effective_commission))
|
||||
base_crypto_atoms = round(crypto_atoms / (1 + effective_commission))
|
||||
commission_amount_sats = crypto_atoms - base_crypto_atoms
|
||||
else:
|
||||
commission_amount_sats = 0
|
||||
|
|
|
|||
208
views_api.py
208
views_api.py
|
|
@ -38,23 +38,14 @@ from .models import (
|
|||
DcaClient,
|
||||
UpdateDcaClientData,
|
||||
CreateDepositData,
|
||||
CreateDepositAPI,
|
||||
DepositAPI,
|
||||
DcaDeposit,
|
||||
UpdateDepositStatusData,
|
||||
ClientBalanceSummary,
|
||||
ClientBalanceSummaryAPI,
|
||||
CreateLamassuConfigData,
|
||||
LamassuConfig,
|
||||
UpdateLamassuConfigData,
|
||||
StoredLamassuTransaction,
|
||||
)
|
||||
from .currency_utils import (
|
||||
gtq_to_centavos,
|
||||
centavos_to_gtq,
|
||||
deposit_db_to_api,
|
||||
balance_summary_db_to_api,
|
||||
)
|
||||
|
||||
satmachineadmin_api_router = APIRouter()
|
||||
|
||||
|
|
@ -96,7 +87,7 @@ async def api_get_dca_client(
|
|||
async def api_get_client_balance(
|
||||
client_id: str,
|
||||
wallet: WalletTypeInfo = Depends(check_super_user),
|
||||
) -> ClientBalanceSummaryAPI:
|
||||
) -> ClientBalanceSummary:
|
||||
"""Get client balance summary"""
|
||||
client = await get_dca_client(client_id)
|
||||
if not client:
|
||||
|
|
@ -104,8 +95,7 @@ async def api_get_client_balance(
|
|||
status_code=HTTPStatus.NOT_FOUND, detail="DCA client not found."
|
||||
)
|
||||
|
||||
balance_db = await get_client_balance_summary(client_id)
|
||||
return ClientBalanceSummaryAPI(**balance_summary_db_to_api(balance_db))
|
||||
return await get_client_balance_summary(client_id)
|
||||
|
||||
|
||||
# DCA Deposit Endpoints
|
||||
|
|
@ -114,31 +104,30 @@ async def api_get_client_balance(
|
|||
@satmachineadmin_api_router.get("/api/v1/dca/deposits")
|
||||
async def api_get_deposits(
|
||||
wallet: WalletTypeInfo = Depends(check_super_user),
|
||||
) -> list[DepositAPI]:
|
||||
) -> list[DcaDeposit]:
|
||||
"""Get all deposits"""
|
||||
deposits_db = await get_all_deposits()
|
||||
return [DepositAPI(**deposit_db_to_api(deposit)) for deposit in deposits_db]
|
||||
return await get_all_deposits()
|
||||
|
||||
|
||||
@satmachineadmin_api_router.get("/api/v1/dca/deposits/{deposit_id}")
|
||||
async def api_get_deposit(
|
||||
deposit_id: str,
|
||||
wallet: WalletTypeInfo = Depends(check_super_user),
|
||||
) -> DepositAPI:
|
||||
) -> DcaDeposit:
|
||||
"""Get a specific deposit"""
|
||||
deposit_db = await get_deposit(deposit_id)
|
||||
if not deposit_db:
|
||||
deposit = await get_deposit(deposit_id)
|
||||
if not deposit:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND, detail="Deposit not found."
|
||||
)
|
||||
return DepositAPI(**deposit_db_to_api(deposit_db))
|
||||
return deposit
|
||||
|
||||
|
||||
@satmachineadmin_api_router.post("/api/v1/dca/deposits", status_code=HTTPStatus.CREATED)
|
||||
async def api_create_deposit(
|
||||
data: CreateDepositAPI,
|
||||
data: CreateDepositData,
|
||||
user: User = Depends(check_super_user),
|
||||
) -> DepositAPI:
|
||||
) -> DcaDeposit:
|
||||
"""Create a new deposit"""
|
||||
# Verify client exists
|
||||
client = await get_dca_client(data.client_id)
|
||||
|
|
@ -147,16 +136,7 @@ async def api_create_deposit(
|
|||
status_code=HTTPStatus.NOT_FOUND, detail="DCA client not found."
|
||||
)
|
||||
|
||||
# Convert GTQ to centavos at API boundary
|
||||
deposit_data = CreateDepositData(
|
||||
client_id=data.client_id,
|
||||
amount=gtq_to_centavos(data.amount_gtq),
|
||||
currency=data.currency,
|
||||
notes=data.notes
|
||||
)
|
||||
|
||||
deposit_db = await create_deposit(deposit_data)
|
||||
return DepositAPI(**deposit_db_to_api(deposit_db))
|
||||
return await create_deposit(data)
|
||||
|
||||
|
||||
@satmachineadmin_api_router.put("/api/v1/dca/deposits/{deposit_id}/status")
|
||||
|
|
@ -164,7 +144,7 @@ async def api_update_deposit_status(
|
|||
deposit_id: str,
|
||||
data: UpdateDepositStatusData,
|
||||
user: User = Depends(check_super_user),
|
||||
) -> DepositAPI:
|
||||
) -> DcaDeposit:
|
||||
"""Update deposit status (e.g., confirm deposit)"""
|
||||
deposit = await get_deposit(deposit_id)
|
||||
if not deposit:
|
||||
|
|
@ -172,13 +152,13 @@ async def api_update_deposit_status(
|
|||
status_code=HTTPStatus.NOT_FOUND, detail="Deposit not found."
|
||||
)
|
||||
|
||||
updated_deposit_db = await update_deposit_status(deposit_id, data)
|
||||
if not updated_deposit_db:
|
||||
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 DepositAPI(**deposit_db_to_api(updated_deposit_db))
|
||||
return updated_deposit
|
||||
|
||||
|
||||
# Transaction Polling Endpoints
|
||||
|
|
@ -253,69 +233,139 @@ async def api_manual_poll(
|
|||
)
|
||||
|
||||
|
||||
@satmachineadmin_api_router.post("/api/v1/dca/test-transaction")
|
||||
async def api_test_transaction(
|
||||
@satmachineadmin_api_router.post("/api/v1/dca/process-transaction/{transaction_id}")
|
||||
async def api_process_specific_transaction(
|
||||
transaction_id: str,
|
||||
user: User = Depends(check_super_user),
|
||||
crypto_atoms: int = 103,
|
||||
commission_percentage: float = 0.03,
|
||||
discount: float = 0.0,
|
||||
) -> dict:
|
||||
"""Test transaction processing with simulated Lamassu transaction data"""
|
||||
):
|
||||
"""
|
||||
Manually process a specific Lamassu transaction by ID, bypassing all status filters.
|
||||
|
||||
This endpoint is useful for processing transactions that were manually settled
|
||||
or had dispense issues but need to be included in DCA distribution.
|
||||
"""
|
||||
try:
|
||||
from .transaction_processor import transaction_processor
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from .crud import get_payments_by_lamassu_transaction
|
||||
|
||||
# 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",
|
||||
}
|
||||
# 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",
|
||||
)
|
||||
|
||||
# Process the mock transaction through the complete DCA flow
|
||||
await transaction_processor.process_transaction(mock_transaction)
|
||||
# Check if transaction was already processed
|
||||
existing_payments = await get_payments_by_lamassu_transaction(transaction_id)
|
||||
if existing_payments:
|
||||
return {
|
||||
"success": False,
|
||||
"already_processed": True,
|
||||
"message": f"Transaction {transaction_id} was already processed with {len(existing_payments)} distributions",
|
||||
"payment_count": len(existing_payments),
|
||||
}
|
||||
|
||||
# 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
|
||||
# Fetch the specific transaction from Lamassu (bypassing all filters)
|
||||
transaction = await transaction_processor.fetch_transaction_by_id(db_config, transaction_id)
|
||||
|
||||
if not transaction:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.NOT_FOUND,
|
||||
detail=f"Transaction {transaction_id} not found in Lamassu database",
|
||||
)
|
||||
|
||||
# Process the transaction through normal DCA flow
|
||||
await transaction_processor.process_transaction(transaction)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "Test transaction processed successfully",
|
||||
"message": f"Transaction {transaction_id} 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,
|
||||
"transaction_id": transaction_id,
|
||||
"status": transaction.get("status"),
|
||||
"dispense": transaction.get("dispense"),
|
||||
"dispense_confirmed": transaction.get("dispense_confirmed"),
|
||||
"crypto_amount": transaction.get("crypto_amount"),
|
||||
"fiat_amount": transaction.get("fiat_amount"),
|
||||
},
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
detail=f"Error processing test transaction: {str(e)}",
|
||||
detail=f"Error processing transaction {transaction_id}: {str(e)}",
|
||||
)
|
||||
|
||||
|
||||
# COMMENTED OUT FOR PRODUCTION - Test transaction endpoint disabled
|
||||
# Uncomment only for development/debugging purposes
|
||||
#
|
||||
# @satmachineadmin_api_router.post("/api/v1/dca/test-transaction")
|
||||
# async def api_test_transaction(
|
||||
# user: User = Depends(check_super_user),
|
||||
# crypto_atoms: int = 103,
|
||||
# commission_percentage: float = 0.03,
|
||||
# discount: float = 0.0,
|
||||
# ) -> dict:
|
||||
# """Test transaction processing with simulated Lamassu transaction data"""
|
||||
# try:
|
||||
# from .transaction_processor import transaction_processor
|
||||
# import uuid
|
||||
# from datetime import datetime, timezone
|
||||
#
|
||||
# # Create a mock transaction that mimics Lamassu database structure
|
||||
# mock_transaction = {
|
||||
# "transaction_id": str(uuid.uuid4())[:8], # Short ID for testing
|
||||
# "crypto_amount": crypto_atoms, # Total sats including commission
|
||||
# "fiat_amount": 100, # Mock fiat amount (100 centavos = 1 GTQ)
|
||||
# "commission_percentage": commission_percentage, # Already as decimal
|
||||
# "discount": discount,
|
||||
# "transaction_time": datetime.now(timezone.utc),
|
||||
# "crypto_code": "BTC",
|
||||
# "fiat_code": "GTQ",
|
||||
# "device_id": "test_device",
|
||||
# "status": "confirmed",
|
||||
# }
|
||||
#
|
||||
# # Process the mock transaction through the complete DCA flow
|
||||
# await transaction_processor.process_transaction(mock_transaction)
|
||||
#
|
||||
# # Calculate commission for response
|
||||
# if commission_percentage > 0:
|
||||
# effective_commission = commission_percentage * (100 - discount) / 100
|
||||
# base_crypto_atoms = int(crypto_atoms / (1 + effective_commission))
|
||||
# commission_amount_sats = crypto_atoms - base_crypto_atoms
|
||||
# else:
|
||||
# base_crypto_atoms = crypto_atoms
|
||||
# commission_amount_sats = 0
|
||||
#
|
||||
# return {
|
||||
# "success": True,
|
||||
# "message": "Test transaction processed successfully",
|
||||
# "transaction_details": {
|
||||
# "transaction_id": mock_transaction["transaction_id"],
|
||||
# "total_amount_sats": crypto_atoms,
|
||||
# "base_amount_sats": base_crypto_atoms,
|
||||
# "commission_amount_sats": commission_amount_sats,
|
||||
# "commission_percentage": commission_percentage
|
||||
# * 100, # Show as percentage
|
||||
# "effective_commission": effective_commission * 100
|
||||
# if commission_percentage > 0
|
||||
# else 0,
|
||||
# "discount": discount,
|
||||
# },
|
||||
# }
|
||||
#
|
||||
# except Exception as e:
|
||||
# raise HTTPException(
|
||||
# status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
|
||||
# detail=f"Error processing test transaction: {str(e)}",
|
||||
# )
|
||||
|
||||
|
||||
# Lamassu Transaction Endpoints
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue