Add DCA CRUD operations and models for clients, deposits, and payments

This commit is contained in:
padreug 2025-06-17 18:29:23 +02:00
parent a8e1918633
commit 22ebdc76bb
3 changed files with 409 additions and 1 deletions

251
crud.py
View file

@ -1,11 +1,18 @@
# Description: This file contains the CRUD operations for talking to the database. # Description: This file contains the CRUD operations for talking to the database.
from typing import List, Optional, Union from typing import List, Optional, Union
from datetime import datetime
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 CreateMyExtensionData, MyExtension from .models import (
CreateMyExtensionData, MyExtension,
CreateDcaClientData, DcaClient, UpdateDcaClientData,
CreateDepositData, DcaDeposit, UpdateDepositStatusData,
CreateDcaPaymentData, DcaPayment,
ClientBalanceSummary
)
db = Database("ext_myextension") db = Database("ext_myextension")
@ -43,3 +50,245 @@ async def delete_myextension(myextension_id: str) -> None:
await db.execute( await db.execute(
"DELETE FROM myextension.maintable WHERE id = :id", {"id": myextension_id} "DELETE FROM myextension.maintable WHERE id = :id", {"id": myextension_id}
) )
# DCA Client CRUD Operations
async def create_dca_client(data: CreateDcaClientData) -> DcaClient:
client_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO myextension.dca_clients
(id, user_id, wallet_id, dca_mode, fixed_mode_daily_limit, status, created_at, updated_at)
VALUES (:id, :user_id, :wallet_id, :dca_mode, :fixed_mode_daily_limit, :status, :created_at, :updated_at)
""",
{
"id": client_id,
"user_id": data.user_id,
"wallet_id": data.wallet_id,
"dca_mode": data.dca_mode,
"fixed_mode_daily_limit": data.fixed_mode_daily_limit,
"status": "active",
"created_at": datetime.now(),
"updated_at": datetime.now()
}
)
return await get_dca_client(client_id)
async def get_dca_client(client_id: str) -> Optional[DcaClient]:
return await db.fetchone(
"SELECT * FROM myextension.dca_clients WHERE id = :id",
{"id": client_id},
DcaClient,
)
async def get_dca_clients() -> List[DcaClient]:
return await db.fetchall(
"SELECT * FROM myextension.dca_clients ORDER BY created_at DESC",
model=DcaClient,
)
async def get_dca_client_by_user(user_id: str) -> Optional[DcaClient]:
return await db.fetchone(
"SELECT * FROM myextension.dca_clients WHERE user_id = :user_id",
{"user_id": user_id},
DcaClient,
)
async def update_dca_client(client_id: str, data: UpdateDcaClientData) -> Optional[DcaClient]:
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 myextension.dca_clients SET {set_clause} WHERE id = :id",
update_data
)
return await get_dca_client(client_id)
async def delete_dca_client(client_id: str) -> None:
await db.execute(
"DELETE FROM myextension.dca_clients WHERE id = :id",
{"id": client_id}
)
# DCA Deposit CRUD Operations
async def create_deposit(data: CreateDepositData) -> DcaDeposit:
deposit_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO myextension.dca_deposits
(id, client_id, amount, currency, status, notes, created_at)
VALUES (:id, :client_id, :amount, :currency, :status, :notes, :created_at)
""",
{
"id": deposit_id,
"client_id": data.client_id,
"amount": data.amount,
"currency": data.currency,
"status": "pending",
"notes": data.notes,
"created_at": datetime.now()
}
)
return await get_deposit(deposit_id)
async def get_deposit(deposit_id: str) -> Optional[DcaDeposit]:
return await db.fetchone(
"SELECT * FROM myextension.dca_deposits WHERE id = :id",
{"id": deposit_id},
DcaDeposit,
)
async def get_deposits_by_client(client_id: str) -> List[DcaDeposit]:
return await db.fetchall(
"SELECT * FROM myextension.dca_deposits WHERE client_id = :client_id ORDER BY created_at DESC",
{"client_id": client_id},
DcaDeposit,
)
async def get_all_deposits() -> List[DcaDeposit]:
return await db.fetchall(
"SELECT * FROM myextension.dca_deposits ORDER BY created_at DESC",
model=DcaDeposit,
)
async def update_deposit_status(deposit_id: str, data: UpdateDepositStatusData) -> Optional[DcaDeposit]:
update_data = {
"status": data.status,
"notes": data.notes
}
if data.status == "confirmed":
update_data["confirmed_at"] = datetime.now()
set_clause = ", ".join([f"{k} = :{k}" for k, v in update_data.items() if v is not None])
filtered_data = {k: v for k, v in update_data.items() if v is not None}
filtered_data["id"] = deposit_id
await db.execute(
f"UPDATE myextension.dca_deposits SET {set_clause} WHERE id = :id",
filtered_data
)
return await get_deposit(deposit_id)
# DCA Payment CRUD Operations
async def create_dca_payment(data: CreateDcaPaymentData) -> DcaPayment:
payment_id = urlsafe_short_hash()
await db.execute(
"""
INSERT INTO myextension.dca_payments
(id, client_id, amount_sats, amount_fiat, exchange_rate, transaction_type,
lamassu_transaction_id, payment_hash, status, created_at)
VALUES (:id, :client_id, :amount_sats, :amount_fiat, :exchange_rate, :transaction_type,
:lamassu_transaction_id, :payment_hash, :status, :created_at)
""",
{
"id": payment_id,
"client_id": data.client_id,
"amount_sats": data.amount_sats,
"amount_fiat": data.amount_fiat,
"exchange_rate": data.exchange_rate,
"transaction_type": data.transaction_type,
"lamassu_transaction_id": data.lamassu_transaction_id,
"payment_hash": data.payment_hash,
"status": "pending",
"created_at": datetime.now()
}
)
return await get_dca_payment(payment_id)
async def get_dca_payment(payment_id: str) -> Optional[DcaPayment]:
return await db.fetchone(
"SELECT * FROM myextension.dca_payments WHERE id = :id",
{"id": payment_id},
DcaPayment,
)
async def get_payments_by_client(client_id: str) -> List[DcaPayment]:
return await db.fetchall(
"SELECT * FROM myextension.dca_payments WHERE client_id = :client_id ORDER BY created_at DESC",
{"client_id": client_id},
DcaPayment,
)
async def get_all_payments() -> List[DcaPayment]:
return await db.fetchall(
"SELECT * FROM myextension.dca_payments ORDER BY created_at DESC",
model=DcaPayment,
)
async def get_payments_by_lamassu_transaction(lamassu_transaction_id: str) -> List[DcaPayment]:
return await db.fetchall(
"SELECT * FROM myextension.dca_payments WHERE lamassu_transaction_id = :transaction_id",
{"transaction_id": lamassu_transaction_id},
DcaPayment,
)
# Balance and Summary Operations
async def get_client_balance_summary(client_id: str) -> ClientBalanceSummary:
# Get total confirmed deposits
total_deposits_result = await db.fetchone(
"""
SELECT COALESCE(SUM(amount), 0) as total, currency
FROM myextension.dca_deposits
WHERE client_id = :client_id AND status = 'confirmed'
GROUP BY currency
""",
{"client_id": client_id}
)
# Get total payments made
total_payments_result = await db.fetchone(
"""
SELECT COALESCE(SUM(amount_fiat), 0) as total
FROM myextension.dca_payments
WHERE client_id = :client_id AND status = 'confirmed'
""",
{"client_id": client_id}
)
total_deposits = total_deposits_result["total"] if total_deposits_result else 0
total_payments = total_payments_result["total"] if total_payments_result else 0
currency = total_deposits_result["currency"] if total_deposits_result else "GTQ"
return ClientBalanceSummary(
client_id=client_id,
total_deposits=total_deposits,
total_payments=total_payments,
remaining_balance=total_deposits - total_payments,
currency=currency
)
async def get_flow_mode_clients() -> List[DcaClient]:
return await db.fetchall(
"SELECT * FROM myextension.dca_clients WHERE dca_mode = 'flow' AND status = 'active'",
model=DcaClient,
)
async def get_fixed_mode_clients() -> List[DcaClient]:
return await db.fetchall(
"SELECT * FROM myextension.dca_clients WHERE dca_mode = 'fixed' AND status = 'active'",
model=DcaClient,
)

View file

@ -31,3 +31,67 @@ async def m002_add_timestamp(db):
ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}; ADD COLUMN created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now};
""" """
) )
async def m003_create_dca_clients(db):
"""
Create DCA clients table.
"""
await db.execute(
f"""
CREATE TABLE myextension.dca_clients (
id TEXT PRIMARY KEY NOT NULL,
user_id TEXT NOT NULL,
wallet_id TEXT NOT NULL,
dca_mode TEXT NOT NULL DEFAULT 'flow',
fixed_mode_daily_limit INTEGER,
status TEXT NOT NULL DEFAULT 'active',
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}
);
"""
)
async def m004_create_dca_deposits(db):
"""
Create DCA deposits table.
"""
await db.execute(
f"""
CREATE TABLE myextension.dca_deposits (
id TEXT PRIMARY KEY NOT NULL,
client_id TEXT NOT NULL,
amount INTEGER NOT NULL,
currency TEXT NOT NULL DEFAULT 'GTQ',
status TEXT NOT NULL DEFAULT 'pending',
notes TEXT,
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
confirmed_at TIMESTAMP,
FOREIGN KEY (client_id) REFERENCES myextension.dca_clients(id)
);
"""
)
async def m005_create_dca_payments(db):
"""
Create DCA payments table.
"""
await db.execute(
f"""
CREATE TABLE myextension.dca_payments (
id TEXT PRIMARY KEY NOT NULL,
client_id TEXT NOT NULL,
amount_sats INTEGER NOT NULL,
amount_fiat INTEGER NOT NULL,
exchange_rate REAL NOT NULL,
transaction_type TEXT NOT NULL,
lamassu_transaction_id TEXT,
payment_hash TEXT,
status TEXT NOT NULL DEFAULT 'pending',
created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now},
FOREIGN KEY (client_id) REFERENCES myextension.dca_clients(id)
);
"""
)

View file

@ -1,10 +1,105 @@
# Description: Pydantic data models dictate what is passed between frontend and backend. # Description: Pydantic data models dictate what is passed between frontend and backend.
from datetime import datetime
from typing import Optional from typing import Optional
from pydantic import BaseModel from pydantic import BaseModel
# DCA Client Models
class CreateDcaClientData(BaseModel):
user_id: str
wallet_id: str
dca_mode: str = "flow" # 'flow' or 'fixed'
fixed_mode_daily_limit: Optional[int] = None
class DcaClient(BaseModel):
id: str
user_id: str
wallet_id: str
dca_mode: str
fixed_mode_daily_limit: Optional[int]
status: str
created_at: datetime
updated_at: datetime
class UpdateDcaClientData(BaseModel):
dca_mode: Optional[str] = None
fixed_mode_daily_limit: Optional[int] = None
status: Optional[str] = None
# Deposit Models
class CreateDepositData(BaseModel):
client_id: str
amount: int # Amount in smallest currency unit (centavos for GTQ)
currency: str = "GTQ"
notes: Optional[str] = None
class DcaDeposit(BaseModel):
id: str
client_id: str
amount: int
currency: str
status: str # 'pending' or 'confirmed'
notes: Optional[str]
created_at: datetime
confirmed_at: Optional[datetime]
class UpdateDepositStatusData(BaseModel):
status: str
notes: Optional[str] = None
# Payment Models
class CreateDcaPaymentData(BaseModel):
client_id: str
amount_sats: int
amount_fiat: int
exchange_rate: float
transaction_type: str # 'flow', 'fixed', 'manual', 'commission'
lamassu_transaction_id: Optional[str] = None
payment_hash: Optional[str] = None
class DcaPayment(BaseModel):
id: str
client_id: str
amount_sats: int
amount_fiat: int
exchange_rate: float
transaction_type: str
lamassu_transaction_id: Optional[str]
payment_hash: Optional[str]
status: str # 'pending', 'confirmed', 'failed'
created_at: datetime
# Client Balance Summary
class ClientBalanceSummary(BaseModel):
client_id: str
total_deposits: int # Total confirmed deposits
total_payments: int # Total payments made
remaining_balance: int # Available balance for DCA
currency: str
# Transaction Processing Models
class LamassuTransaction(BaseModel):
transaction_id: str
amount_fiat: int
amount_crypto: int
exchange_rate: float
transaction_type: str # 'cash_in' or 'cash_out'
status: str
timestamp: datetime
# Legacy models (keep for backward compatibility during transition)
class CreateMyExtensionData(BaseModel): class CreateMyExtensionData(BaseModel):
id: Optional[str] = "" id: Optional[str] = ""
name: str name: str