fix: add SQLite compatibility for Decimal types
SQLite doesn't support Decimal natively - it stores DECIMAL columns as REAL (float). This caused sqlite3.ProgrammingError when writing Decimal values. Changes: - Add prepare_for_db() helper to convert Decimal→float before writes - Add Pydantic validators to convert float→Decimal on model creation - Update CRUD layer tests to verify float params for SQLite - Add SQLite round-trip tests to verify precision is preserved The data flow is now: Decimal (calculations) → float (prepare_for_db) → SQLite → float → Decimal (validators) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d245047487
commit
904b3f1d61
3 changed files with 323 additions and 60 deletions
70
crud.py
70
crud.py
|
|
@ -1,5 +1,6 @@
|
|||
# Description: This file contains the CRUD operations for talking to the database.
|
||||
|
||||
from decimal import Decimal
|
||||
from typing import List, Optional, Union
|
||||
from datetime import datetime, timezone
|
||||
|
||||
|
|
@ -18,16 +19,33 @@ from .models import (
|
|||
db = Database("ext_satoshimachine")
|
||||
|
||||
|
||||
def prepare_for_db(values: dict) -> dict:
|
||||
"""
|
||||
Convert Decimal values to float for SQLite compatibility.
|
||||
|
||||
SQLite doesn't support Decimal natively - it stores DECIMAL as REAL (float).
|
||||
This function converts Decimal values to float before database writes.
|
||||
The Pydantic models handle converting float back to Decimal on read.
|
||||
"""
|
||||
result = {}
|
||||
for k, v in values.items():
|
||||
if isinstance(v, Decimal):
|
||||
result[k] = float(v)
|
||||
else:
|
||||
result[k] = v
|
||||
return result
|
||||
|
||||
|
||||
# DCA Client CRUD Operations
|
||||
async def create_dca_client(data: CreateDcaClientData) -> DcaClient:
|
||||
client_id = urlsafe_short_hash()
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO satoshimachine.dca_clients
|
||||
INSERT INTO satoshimachine.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)
|
||||
""",
|
||||
{
|
||||
prepare_for_db({
|
||||
"id": client_id,
|
||||
"user_id": data.user_id,
|
||||
"wallet_id": data.wallet_id,
|
||||
|
|
@ -37,7 +55,7 @@ async def create_dca_client(data: CreateDcaClientData) -> DcaClient:
|
|||
"status": "active",
|
||||
"created_at": datetime.now(),
|
||||
"updated_at": datetime.now()
|
||||
}
|
||||
})
|
||||
)
|
||||
return await get_dca_client(client_id)
|
||||
|
||||
|
|
@ -69,14 +87,14 @@ async def update_dca_client(client_id: str, data: UpdateDcaClientData) -> Option
|
|||
update_data = {k: v for k, v in data.dict().items() if v is not None}
|
||||
if not update_data:
|
||||
return await get_dca_client(client_id)
|
||||
|
||||
|
||||
update_data["updated_at"] = datetime.now()
|
||||
set_clause = ", ".join([f"{k} = :{k}" for k in update_data.keys()])
|
||||
update_data["id"] = client_id
|
||||
|
||||
|
||||
await db.execute(
|
||||
f"UPDATE satoshimachine.dca_clients SET {set_clause} WHERE id = :id",
|
||||
update_data
|
||||
prepare_for_db(update_data)
|
||||
)
|
||||
return await get_dca_client(client_id)
|
||||
|
||||
|
|
@ -93,11 +111,11 @@ async def create_deposit(data: CreateDepositData) -> DcaDeposit:
|
|||
deposit_id = urlsafe_short_hash()
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO satoshimachine.dca_deposits
|
||||
INSERT INTO satoshimachine.dca_deposits
|
||||
(id, client_id, amount, currency, status, notes, created_at)
|
||||
VALUES (:id, :client_id, :amount, :currency, :status, :notes, :created_at)
|
||||
""",
|
||||
{
|
||||
prepare_for_db({
|
||||
"id": deposit_id,
|
||||
"client_id": data.client_id,
|
||||
"amount": data.amount,
|
||||
|
|
@ -105,7 +123,7 @@ async def create_deposit(data: CreateDepositData) -> DcaDeposit:
|
|||
"status": "pending",
|
||||
"notes": data.notes,
|
||||
"created_at": datetime.now()
|
||||
}
|
||||
})
|
||||
)
|
||||
return await get_deposit(deposit_id)
|
||||
|
||||
|
|
@ -158,13 +176,13 @@ async def create_dca_payment(data: CreateDcaPaymentData) -> DcaPayment:
|
|||
payment_id = urlsafe_short_hash()
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO satoshimachine.dca_payments
|
||||
(id, client_id, amount_sats, amount_fiat, exchange_rate, transaction_type,
|
||||
INSERT INTO satoshimachine.dca_payments
|
||||
(id, client_id, amount_sats, amount_fiat, exchange_rate, transaction_type,
|
||||
lamassu_transaction_id, payment_hash, status, created_at, transaction_time)
|
||||
VALUES (:id, :client_id, :amount_sats, :amount_fiat, :exchange_rate, :transaction_type,
|
||||
:lamassu_transaction_id, :payment_hash, :status, :created_at, :transaction_time)
|
||||
""",
|
||||
{
|
||||
prepare_for_db({
|
||||
"id": payment_id,
|
||||
"client_id": data.client_id,
|
||||
"amount_sats": data.amount_sats,
|
||||
|
|
@ -176,7 +194,7 @@ async def create_dca_payment(data: CreateDcaPaymentData) -> DcaPayment:
|
|||
"status": "pending",
|
||||
"created_at": datetime.now(),
|
||||
"transaction_time": data.transaction_time
|
||||
}
|
||||
})
|
||||
)
|
||||
return await get_dca_payment(payment_id)
|
||||
|
||||
|
|
@ -295,22 +313,22 @@ async def get_fixed_mode_clients() -> List[DcaClient]:
|
|||
# Lamassu Configuration CRUD Operations
|
||||
async def create_lamassu_config(data: CreateLamassuConfigData) -> LamassuConfig:
|
||||
config_id = urlsafe_short_hash()
|
||||
|
||||
|
||||
# Deactivate any existing configs first (only one active config allowed)
|
||||
await db.execute(
|
||||
"UPDATE satoshimachine.lamassu_config SET is_active = false, updated_at = :updated_at",
|
||||
{"updated_at": datetime.now()}
|
||||
)
|
||||
|
||||
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO satoshimachine.lamassu_config
|
||||
INSERT INTO satoshimachine.lamassu_config
|
||||
(id, host, port, database_name, username, password, source_wallet_id, commission_wallet_id, is_active, created_at, updated_at,
|
||||
use_ssh_tunnel, ssh_host, ssh_port, ssh_username, ssh_password, ssh_private_key, max_daily_limit_gtq)
|
||||
VALUES (:id, :host, :port, :database_name, :username, :password, :source_wallet_id, :commission_wallet_id, :is_active, :created_at, :updated_at,
|
||||
:use_ssh_tunnel, :ssh_host, :ssh_port, :ssh_username, :ssh_password, :ssh_private_key, :max_daily_limit_gtq)
|
||||
""",
|
||||
{
|
||||
prepare_for_db({
|
||||
"id": config_id,
|
||||
"host": data.host,
|
||||
"port": data.port,
|
||||
|
|
@ -329,7 +347,7 @@ async def create_lamassu_config(data: CreateLamassuConfigData) -> LamassuConfig:
|
|||
"ssh_password": data.ssh_password,
|
||||
"ssh_private_key": data.ssh_private_key,
|
||||
"max_daily_limit_gtq": data.max_daily_limit_gtq
|
||||
}
|
||||
})
|
||||
)
|
||||
return await get_lamassu_config(config_id)
|
||||
|
||||
|
|
@ -360,14 +378,14 @@ async def update_lamassu_config(config_id: str, data: UpdateLamassuConfigData) -
|
|||
update_data = {k: v for k, v in data.dict().items() if v is not None}
|
||||
if not update_data:
|
||||
return await get_lamassu_config(config_id)
|
||||
|
||||
|
||||
update_data["updated_at"] = datetime.now()
|
||||
set_clause = ", ".join([f"{k} = :{k}" for k in update_data.keys()])
|
||||
update_data["id"] = config_id
|
||||
|
||||
|
||||
await db.execute(
|
||||
f"UPDATE satoshimachine.lamassu_config SET {set_clause} WHERE id = :id",
|
||||
update_data
|
||||
prepare_for_db(update_data)
|
||||
)
|
||||
return await get_lamassu_config(config_id)
|
||||
|
||||
|
|
@ -436,9 +454,9 @@ async def create_lamassu_transaction(data: CreateLamassuTransactionData) -> Stor
|
|||
transaction_id = urlsafe_short_hash()
|
||||
await db.execute(
|
||||
"""
|
||||
INSERT INTO satoshimachine.lamassu_transactions
|
||||
(id, lamassu_transaction_id, fiat_amount, crypto_amount, commission_percentage,
|
||||
discount, effective_commission, commission_amount_sats, base_amount_sats,
|
||||
INSERT INTO satoshimachine.lamassu_transactions
|
||||
(id, lamassu_transaction_id, fiat_amount, crypto_amount, commission_percentage,
|
||||
discount, effective_commission, commission_amount_sats, base_amount_sats,
|
||||
exchange_rate, crypto_code, fiat_code, device_id, transaction_time, processed_at,
|
||||
clients_count, distributions_total_sats)
|
||||
VALUES (:id, :lamassu_transaction_id, :fiat_amount, :crypto_amount, :commission_percentage,
|
||||
|
|
@ -446,7 +464,7 @@ async def create_lamassu_transaction(data: CreateLamassuTransactionData) -> Stor
|
|||
:exchange_rate, :crypto_code, :fiat_code, :device_id, :transaction_time, :processed_at,
|
||||
:clients_count, :distributions_total_sats)
|
||||
""",
|
||||
{
|
||||
prepare_for_db({
|
||||
"id": transaction_id,
|
||||
"lamassu_transaction_id": data.lamassu_transaction_id,
|
||||
"fiat_amount": data.fiat_amount,
|
||||
|
|
@ -464,7 +482,7 @@ async def create_lamassu_transaction(data: CreateLamassuTransactionData) -> Stor
|
|||
"processed_at": datetime.now(),
|
||||
"clients_count": 0, # Will be updated after distributions
|
||||
"distributions_total_sats": 0 # Will be updated after distributions
|
||||
}
|
||||
})
|
||||
)
|
||||
return await get_lamassu_transaction(transaction_id)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue