01 Refactor currency handling to store amounts in GTQ: Removed currency conversion utilities, updated models and API endpoints to directly handle GTQ amounts, and modified transaction processing logic for consistency. Enhanced frontend to reflect these changes, ensuring accurate display and submission of GTQ values across the application.

Refactor GTQ storage migration: Moved the conversion logic for centavo amounts to GTQ into a new migration function, m004_convert_to_gtq_storage, ensuring proper data type changes and updates across relevant tables. This enhances clarity and maintains the integrity of the migration process.
This commit is contained in:
padreug 2025-07-06 00:00:30 +02:00
parent aa71321c84
commit c83ebf43ab
8 changed files with 157 additions and 162 deletions

View file

@ -267,7 +267,7 @@ async def get_client_balance_summary(client_id: str, as_of_time: Optional[dateti
from loguru import logger from loguru import logger
# Verify timezone consistency for temporal filtering # Verify timezone consistency for temporal filtering
tz_info = "UTC" if as_of_time.tzinfo == timezone.utc else f"TZ: {as_of_time.tzinfo}" 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( return ClientBalanceSummary(
client_id=client_id, client_id=client_id,

View file

@ -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
}

View file

@ -134,4 +134,88 @@ async def m003_add_max_daily_limit_config(db):
ALTER TABLE satoshimachine.lamassu_config ALTER TABLE satoshimachine.lamassu_config
ADD COLUMN max_daily_limit_gtq INTEGER NOT NULL DEFAULT 2000 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.
"""
# Convert dca_deposits amounts from centavos to GTQ
await db.execute(
"""
UPDATE satoshimachine.dca_deposits
SET amount = CAST(amount AS DECIMAL(10,2)) / 100.0
WHERE currency = 'GTQ'
"""
)
# Convert dca_payments amounts from centavos to GTQ
await db.execute(
"""
UPDATE satoshimachine.dca_payments
SET amount_fiat = CAST(amount_fiat AS DECIMAL(10,2)) / 100.0
"""
)
# Convert lamassu_transactions amounts from centavos to GTQ
await db.execute(
"""
UPDATE satoshimachine.lamassu_transactions
SET fiat_amount = CAST(fiat_amount AS DECIMAL(10,2)) / 100.0
"""
)
# Convert fixed_mode_daily_limit from centavos to GTQ
await db.execute(
"""
UPDATE satoshimachine.dca_clients
SET fixed_mode_daily_limit = CAST(fixed_mode_daily_limit AS DECIMAL(10,2)) / 100.0
WHERE fixed_mode_daily_limit IS NOT NULL
"""
)
# Convert max_daily_limit_gtq in config (if already in centavos)
await db.execute(
"""
UPDATE satoshimachine.lamassu_config
SET max_daily_limit_gtq = CAST(max_daily_limit_gtq AS DECIMAL(10,2)) / 100.0
WHERE max_daily_limit_gtq > 1000
"""
)
# Change column types to DECIMAL
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)
"""
) )

View file

@ -3,7 +3,7 @@
from datetime import datetime from datetime import datetime
from typing import Optional from typing import Optional
from pydantic import BaseModel from pydantic import BaseModel, validator
# DCA Client Models # DCA Client Models
@ -12,7 +12,7 @@ class CreateDcaClientData(BaseModel):
wallet_id: str wallet_id: str
username: str username: str
dca_mode: str = "flow" # 'flow' or 'fixed' dca_mode: str = "flow" # 'flow' or 'fixed'
fixed_mode_daily_limit: Optional[int] = None fixed_mode_daily_limit: Optional[float] = None
class DcaClient(BaseModel): class DcaClient(BaseModel):
@ -30,45 +30,29 @@ class DcaClient(BaseModel):
class UpdateDcaClientData(BaseModel): class UpdateDcaClientData(BaseModel):
username: Optional[str] = None username: Optional[str] = None
dca_mode: 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 status: Optional[str] = None
# API Models for Deposits (Frontend <-> Backend communication) # Deposit Models (Now storing GTQ directly)
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)
class CreateDepositData(BaseModel): class CreateDepositData(BaseModel):
"""Internal model - database stores centavos"""
client_id: str 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" currency: str = "GTQ"
notes: Optional[str] = None 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): class DcaDeposit(BaseModel):
"""Internal model - database stores centavos"""
id: str id: str
client_id: str client_id: str
amount: int amount: float # Amount in GTQ (e.g., 150.75)
currency: str currency: str
status: str # 'pending' or 'confirmed' status: str # 'pending' or 'confirmed'
notes: Optional[str] notes: Optional[str]
@ -85,7 +69,7 @@ class UpdateDepositStatusData(BaseModel):
class CreateDcaPaymentData(BaseModel): class CreateDcaPaymentData(BaseModel):
client_id: str client_id: str
amount_sats: int 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 exchange_rate: float
transaction_type: str # 'flow', 'fixed', 'manual', 'commission' transaction_type: str # 'flow', 'fixed', 'manual', 'commission'
lamassu_transaction_id: Optional[str] = None lamassu_transaction_id: Optional[str] = None
@ -97,7 +81,7 @@ class DcaPayment(BaseModel):
id: str id: str
client_id: str client_id: str
amount_sats: int 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 exchange_rate: float
transaction_type: str transaction_type: str
lamassu_transaction_id: Optional[str] lamassu_transaction_id: Optional[str]
@ -107,30 +91,19 @@ class DcaPayment(BaseModel):
transaction_time: Optional[datetime] = None # Original ATM transaction time transaction_time: Optional[datetime] = None # Original ATM transaction time
# API Models for Client Balance Summary # Client Balance Summary (Now storing GTQ directly)
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
class ClientBalanceSummary(BaseModel): class ClientBalanceSummary(BaseModel):
"""Internal model - stores centavos"""
client_id: str client_id: str
total_deposits: int # Total confirmed deposits total_deposits: float # Total confirmed deposits in GTQ
total_payments: int # Total payments made total_payments: float # Total payments made in GTQ
remaining_balance: int # Available balance for DCA remaining_balance: float # Available balance for DCA in GTQ
currency: str currency: str
# Transaction Processing Models # Transaction Processing Models
class LamassuTransaction(BaseModel): class LamassuTransaction(BaseModel):
transaction_id: str 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 amount_crypto: int
exchange_rate: float exchange_rate: float
transaction_type: str # 'cash_in' or 'cash_out' transaction_type: str # 'cash_in' or 'cash_out'
@ -141,7 +114,7 @@ class LamassuTransaction(BaseModel):
# Lamassu Transaction Storage Models # Lamassu Transaction Storage Models
class CreateLamassuTransactionData(BaseModel): class CreateLamassuTransactionData(BaseModel):
lamassu_transaction_id: str 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 crypto_amount: int
commission_percentage: float commission_percentage: float
discount: float = 0.0 discount: float = 0.0
@ -158,7 +131,7 @@ class CreateLamassuTransactionData(BaseModel):
class StoredLamassuTransaction(BaseModel): class StoredLamassuTransaction(BaseModel):
id: str id: str
lamassu_transaction_id: str lamassu_transaction_id: str
fiat_amount: int fiat_amount: float # Amount in GTQ (e.g., 150.75)
crypto_amount: int crypto_amount: int
commission_percentage: float commission_percentage: float
discount: float discount: float
@ -194,7 +167,14 @@ class CreateLamassuConfigData(BaseModel):
ssh_password: Optional[str] = None ssh_password: Optional[str] = None
ssh_private_key: Optional[str] = None # Path to private key file or key content ssh_private_key: Optional[str] = None # Path to private key file or key content
# DCA Client Limits # 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): class LamassuConfig(BaseModel):
@ -224,7 +204,7 @@ class LamassuConfig(BaseModel):
last_poll_time: Optional[datetime] = None last_poll_time: Optional[datetime] = None
last_successful_poll: Optional[datetime] = None last_successful_poll: Optional[datetime] = None
# DCA Client Limits # 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): class UpdateLamassuConfigData(BaseModel):

View file

@ -27,7 +27,7 @@ window.app = Vue.createApp({
depositsTable: { depositsTable: {
columns: [ columns: [
{ name: 'client_id', align: 'left', label: 'Client', field: 'client_id' }, { 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: 'currency', align: 'left', label: 'Currency', field: 'currency' },
{ name: 'status', align: 'left', label: 'Status', field: 'status' }, { name: 'status', align: 'left', label: 'Status', field: 'status' },
{ name: 'created_at', align: 'left', label: 'Created', field: 'created_at' }, { name: 'created_at', align: 'left', label: 'Created', field: 'created_at' },
@ -130,11 +130,11 @@ window.app = Vue.createApp({
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
methods: { methods: {
// Utility Methods - Simplified since API handles conversion // Utility Methods
formatCurrency(amount) { formatCurrency(amount) {
if (!amount) return 'Q 0.00'; 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', { return new Intl.NumberFormat('es-GT', {
style: 'currency', style: 'currency',
currency: 'GTQ', currency: 'GTQ',
@ -279,7 +279,7 @@ window.app = Vue.createApp({
) )
return { return {
...client, ...client,
remaining_balance: balance.remaining_balance_gtq remaining_balance: balance.remaining_balance
} }
} catch (error) { } catch (error) {
console.error(`Error fetching balance for client ${client.id}:`, error) console.error(`Error fetching balance for client ${client.id}:`, error)
@ -303,7 +303,7 @@ window.app = Vue.createApp({
try { try {
const data = { const data = {
client_id: this.quickDepositForm.selectedClient?.value, 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', currency: 'GTQ',
notes: this.quickDepositForm.notes notes: this.quickDepositForm.notes
} }
@ -376,7 +376,7 @@ window.app = Vue.createApp({
try { try {
const data = { const data = {
client_id: this.depositFormDialog.data.client_id, 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, currency: this.depositFormDialog.data.currency,
notes: this.depositFormDialog.data.notes notes: this.depositFormDialog.data.notes
} }

View file

@ -173,7 +173,7 @@
<q-tr :props="props"> <q-tr :props="props">
<q-td v-for="col in props.cols" :key="col.name" :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-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'"> <div v-else-if="col.field == 'status'">
<q-badge :color="col.value === 'confirmed' ? 'green' : 'orange'"> <q-badge :color="col.value === 'confirmed' ? 'green' : 'orange'">
${ col.value } ${ col.value }
@ -470,9 +470,9 @@
<q-item-section> <q-item-section>
<q-item-label caption>Balance Summary</q-item-label> <q-item-label caption>Balance Summary</q-item-label>
<q-item-label v-if="clientDetailsDialog.balance"> <q-item-label v-if="clientDetailsDialog.balance">
Deposits: ${ formatCurrency(clientDetailsDialog.balance.total_deposits_gtq) } | Deposits: ${ formatCurrency(clientDetailsDialog.balance.total_deposits) } |
Payments: ${ formatCurrency(clientDetailsDialog.balance.total_payments_gtq) } | Payments: ${ formatCurrency(clientDetailsDialog.balance.total_payments) } |
Remaining: ${ formatCurrency(clientDetailsDialog.balance.remaining_balance_gtq) } Remaining: ${ formatCurrency(clientDetailsDialog.balance.remaining_balance) }
</q-item-label> </q-item-label>
</q-item-section> </q-item-section>
</q-item> </q-item>

View file

@ -671,7 +671,7 @@ class LamassuTransactionProcessor:
# Since crypto_atoms already includes commission, we need to extract the base amount # Since crypto_atoms already includes commission, we need to extract the base amount
# Formula: crypto_atoms = base_amount * (1 + effective_commission) # Formula: crypto_atoms = base_amount * (1 + effective_commission)
# Therefore: base_amount = crypto_atoms / (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 commission_amount_sats = crypto_atoms - base_crypto_atoms
else: else:
effective_commission = 0.0 effective_commission = 0.0
@ -755,9 +755,8 @@ class LamassuTransactionProcessor:
client_sats_amount = calc['allocated_sats'] client_sats_amount = calc['allocated_sats']
proportion = calc['proportion'] proportion = calc['proportion']
# Calculate equivalent fiat value in centavos for tracking purposes (industry standard) # Calculate equivalent fiat value in GTQ for tracking purposes
# Store as centavos to maintain precision and avoid floating-point errors client_fiat_amount = round(client_sats_amount / exchange_rate, 2) if exchange_rate > 0 else 0.0
client_fiat_amount = round(client_sats_amount * 100 / exchange_rate) if exchange_rate > 0 else 0
distributions[client_id] = { distributions[client_id] = {
"fiat_amount": client_fiat_amount, "fiat_amount": client_fiat_amount,
@ -765,7 +764,7 @@ class LamassuTransactionProcessor:
"exchange_rate": exchange_rate "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 # Verification: ensure total distribution equals base amount
total_distributed = sum(dist["sats_amount"] for dist in distributions.values()) total_distributed = sum(dist["sats_amount"] for dist in distributions.values())
@ -781,9 +780,9 @@ class LamassuTransactionProcessor:
current_balance = await get_client_balance_summary(client_id) current_balance = await get_client_balance_summary(client_id)
if current_balance.remaining_balance > 0: if current_balance.remaining_balance > 0:
final_distributions[client_id] = distribution 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: 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): if len(final_distributions) != len(distributions):
logger.warning(f"Rejected {len(distributions) - len(final_distributions)} clients due to negative balances during final check") logger.warning(f"Rejected {len(distributions) - len(final_distributions)} clients due to negative balances during final check")
@ -830,22 +829,22 @@ class LamassuTransactionProcessor:
# Final safety check: Verify client still has positive balance before payment # Final safety check: Verify client still has positive balance before payment
current_balance = await get_client_balance_summary(client_id) current_balance = await get_client_balance_summary(client_id)
if current_balance.remaining_balance <= 0: 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 continue
# Verify balance is sufficient for this distribution # Verify balance is sufficient for this distribution
fiat_equivalent = distribution["fiat_amount"] # Already in centavos fiat_equivalent = distribution["fiat_amount"] # Amount in GTQ
if current_balance.remaining_balance < fiat_equivalent: 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") logger.error(f"CRITICAL: Client {client_id[:8]}... insufficient balance ({current_balance.remaining_balance:.2f} < {fiat_equivalent:.2f} GTQ) - REFUSING payment")
continue 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 # Create DCA payment record
payment_data = CreateDcaPaymentData( payment_data = CreateDcaPaymentData(
client_id=client_id, client_id=client_id,
amount_sats=distribution["sats_amount"], 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"], exchange_rate=distribution["exchange_rate"],
transaction_type="flow", transaction_type="flow",
lamassu_transaction_id=transaction_id, lamassu_transaction_id=transaction_id,
@ -888,12 +887,9 @@ class LamassuTransactionProcessor:
return False return False
# Create descriptive memo with DCA metrics # 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) 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) # Calculate cost basis (fiat per BTC)
if exchange_rate > 0: if exchange_rate > 0:
# exchange_rate is sats per fiat unit, so convert to fiat per BTC # exchange_rate is sats per fiat unit, so convert to fiat per BTC
@ -1011,7 +1007,7 @@ class LamassuTransactionProcessor:
# Calculate commission metrics # Calculate commission metrics
if commission_percentage > 0: if commission_percentage > 0:
effective_commission = commission_percentage * (100 - discount) / 100 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 commission_amount_sats = crypto_atoms - base_crypto_atoms
else: else:
effective_commission = 0.0 effective_commission = 0.0
@ -1021,10 +1017,10 @@ class LamassuTransactionProcessor:
# Calculate exchange rate # Calculate exchange rate
exchange_rate = base_crypto_atoms / fiat_amount if fiat_amount > 0 else 0 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( transaction_data = CreateLamassuTransactionData(
lamassu_transaction_id=transaction["transaction_id"], 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, crypto_amount=crypto_atoms,
commission_percentage=commission_percentage, commission_percentage=commission_percentage,
discount=discount, discount=discount,
@ -1137,7 +1133,7 @@ class LamassuTransactionProcessor:
if commission_percentage and commission_percentage > 0: if commission_percentage and commission_percentage > 0:
effective_commission = commission_percentage * (100 - discount) / 100 effective_commission = commission_percentage * (100 - discount) / 100
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 commission_amount_sats = crypto_atoms - base_crypto_atoms
else: else:
commission_amount_sats = 0 commission_amount_sats = 0

View file

@ -38,23 +38,14 @@ from .models import (
DcaClient, DcaClient,
UpdateDcaClientData, UpdateDcaClientData,
CreateDepositData, CreateDepositData,
CreateDepositAPI,
DepositAPI,
DcaDeposit, DcaDeposit,
UpdateDepositStatusData, UpdateDepositStatusData,
ClientBalanceSummary, ClientBalanceSummary,
ClientBalanceSummaryAPI,
CreateLamassuConfigData, CreateLamassuConfigData,
LamassuConfig, LamassuConfig,
UpdateLamassuConfigData, UpdateLamassuConfigData,
StoredLamassuTransaction, StoredLamassuTransaction,
) )
from .currency_utils import (
gtq_to_centavos,
centavos_to_gtq,
deposit_db_to_api,
balance_summary_db_to_api,
)
satmachineadmin_api_router = APIRouter() satmachineadmin_api_router = APIRouter()
@ -96,7 +87,7 @@ async def api_get_dca_client(
async def api_get_client_balance( async def api_get_client_balance(
client_id: str, client_id: str,
wallet: WalletTypeInfo = Depends(check_super_user), wallet: WalletTypeInfo = Depends(check_super_user),
) -> ClientBalanceSummaryAPI: ) -> ClientBalanceSummary:
"""Get client balance summary""" """Get client balance summary"""
client = await get_dca_client(client_id) client = await get_dca_client(client_id)
if not client: if not client:
@ -104,8 +95,7 @@ async def api_get_client_balance(
status_code=HTTPStatus.NOT_FOUND, detail="DCA client not found." status_code=HTTPStatus.NOT_FOUND, detail="DCA client not found."
) )
balance_db = await get_client_balance_summary(client_id) return await get_client_balance_summary(client_id)
return ClientBalanceSummaryAPI(**balance_summary_db_to_api(balance_db))
# DCA Deposit Endpoints # DCA Deposit Endpoints
@ -114,31 +104,30 @@ async def api_get_client_balance(
@satmachineadmin_api_router.get("/api/v1/dca/deposits") @satmachineadmin_api_router.get("/api/v1/dca/deposits")
async def api_get_deposits( async def api_get_deposits(
wallet: WalletTypeInfo = Depends(check_super_user), wallet: WalletTypeInfo = Depends(check_super_user),
) -> list[DepositAPI]: ) -> list[DcaDeposit]:
"""Get all deposits""" """Get all deposits"""
deposits_db = await get_all_deposits() return await get_all_deposits()
return [DepositAPI(**deposit_db_to_api(deposit)) for deposit in deposits_db]
@satmachineadmin_api_router.get("/api/v1/dca/deposits/{deposit_id}") @satmachineadmin_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(check_super_user), wallet: WalletTypeInfo = Depends(check_super_user),
) -> DepositAPI: ) -> DcaDeposit:
"""Get a specific deposit""" """Get a specific deposit"""
deposit_db = await get_deposit(deposit_id) deposit = await get_deposit(deposit_id)
if not deposit_db: if not deposit:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Deposit not found." 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) @satmachineadmin_api_router.post("/api/v1/dca/deposits", status_code=HTTPStatus.CREATED)
async def api_create_deposit( async def api_create_deposit(
data: CreateDepositAPI, data: CreateDepositData,
user: User = Depends(check_super_user), user: User = Depends(check_super_user),
) -> DepositAPI: ) -> DcaDeposit:
"""Create a new deposit""" """Create a new deposit"""
# Verify client exists # Verify client exists
client = await get_dca_client(data.client_id) 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." status_code=HTTPStatus.NOT_FOUND, detail="DCA client not found."
) )
# Convert GTQ to centavos at API boundary return await create_deposit(data)
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))
@satmachineadmin_api_router.put("/api/v1/dca/deposits/{deposit_id}/status") @satmachineadmin_api_router.put("/api/v1/dca/deposits/{deposit_id}/status")
@ -164,7 +144,7 @@ async def api_update_deposit_status(
deposit_id: str, deposit_id: str,
data: UpdateDepositStatusData, data: UpdateDepositStatusData,
user: User = Depends(check_super_user), user: User = Depends(check_super_user),
) -> DepositAPI: ) -> DcaDeposit:
"""Update deposit status (e.g., confirm deposit)""" """Update deposit status (e.g., confirm deposit)"""
deposit = await get_deposit(deposit_id) deposit = await get_deposit(deposit_id)
if not deposit: if not deposit:
@ -172,13 +152,13 @@ async def api_update_deposit_status(
status_code=HTTPStatus.NOT_FOUND, detail="Deposit not found." status_code=HTTPStatus.NOT_FOUND, detail="Deposit not found."
) )
updated_deposit_db = await update_deposit_status(deposit_id, data) updated_deposit = await update_deposit_status(deposit_id, data)
if not updated_deposit_db: if not updated_deposit:
raise HTTPException( raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Failed to update deposit.", detail="Failed to update deposit.",
) )
return DepositAPI(**deposit_db_to_api(updated_deposit_db)) return updated_deposit
# Transaction Polling Endpoints # Transaction Polling Endpoints