Add DCA CRUD operations and models for clients, deposits, and payments
This commit is contained in:
parent
a8e1918633
commit
22ebdc76bb
3 changed files with 409 additions and 1 deletions
251
crud.py
251
crud.py
|
|
@ -1,11 +1,18 @@
|
|||
# Description: This file contains the CRUD operations for talking to the database.
|
||||
|
||||
from typing import List, Optional, Union
|
||||
from datetime import datetime
|
||||
|
||||
from lnbits.db import Database
|
||||
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")
|
||||
|
||||
|
|
@ -43,3 +50,245 @@ async def delete_myextension(myextension_id: str) -> None:
|
|||
await db.execute(
|
||||
"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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -31,3 +31,67 @@ async def m002_add_timestamp(db):
|
|||
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)
|
||||
);
|
||||
"""
|
||||
)
|
||||
|
|
|
|||
95
models.py
95
models.py
|
|
@ -1,10 +1,105 @@
|
|||
# Description: Pydantic data models dictate what is passed between frontend and backend.
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
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):
|
||||
id: Optional[str] = ""
|
||||
name: str
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue